├── README.md ├── ot.js ├── index.js ├── server.js ├── socketio-adapter.js ├── wrapped-operation.js ├── ajax-adapter.js ├── selection.js ├── undo-manager.js ├── editor-socketio-server.js ├── simple-text-operation.js ├── client.js ├── editor-client.js ├── codemirror-adapter.js └── text-operation.js ├── server.js ├── package.json ├── LICENSE ├── .gitignore └── index.html /README.md: -------------------------------------------------------------------------------- 1 | # ot.js-demo 2 | simple demo for ot.js 3 | 4 | ## Development 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | ## License 11 | MIT 12 | -------------------------------------------------------------------------------- /ot.js/index.js: -------------------------------------------------------------------------------- 1 | exports.version = '0.0.15'; 2 | 3 | exports.TextOperation = require('./text-operation'); 4 | exports.SimpleTextOperation = require('./simple-text-operation'); 5 | exports.Client = require('./client'); 6 | exports.Server = require('./server'); 7 | exports.Selection = require('./selection'); 8 | exports.EditorSocketIOServer = require('./editor-socketio-server'); 9 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var http = require('http').Server(app); 4 | var io = require('socket.io')(http); 5 | 6 | app.get('/', function(req, res){ 7 | res.sendFile(__dirname + '/index.html'); 8 | }); 9 | 10 | app.use('/ot.js', express.static('ot.js')); 11 | app.use('/node_modules', express.static('node_modules')); 12 | 13 | http.listen(3000, function(){ 14 | console.log('listening on *:3000'); 15 | }); 16 | 17 | var EditorSocketIOServer = require('./ot.js/editor-socketio-server.js'); 18 | var server = new EditorSocketIOServer("", [], 1); 19 | 20 | io.on('connection', function(socket) { 21 | server.addClient(socket); 22 | }); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ot.js-demo", 3 | "version": "1.0.0", 4 | "description": "simple demo for ot.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/YingshanDeng/ot.js-demo.git" 13 | }, 14 | "keywords": [ 15 | "demo", 16 | "ot.js", 17 | "Operational", 18 | "transformation", 19 | "OT" 20 | ], 21 | "author": "YingshanDeng ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/YingshanDeng/ot.js-demo/issues" 25 | }, 26 | "homepage": "https://github.com/YingshanDeng/ot.js-demo#readme", 27 | "dependencies": { 28 | "codemirror": "^5.34.0", 29 | "express": "^4.16.2", 30 | "socket.io": "^2.0.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 邓映山 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ot.js Demo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /ot.js/server.js: -------------------------------------------------------------------------------- 1 | if (typeof ot === 'undefined') { 2 | var ot = {}; 3 | } 4 | 5 | ot.Server = (function (global) { 6 | 'use strict'; 7 | 8 | // Constructor. Takes the current document as a string and optionally the array 9 | // of all operations. 10 | function Server (document, operations) { 11 | this.document = document; 12 | this.operations = operations || []; 13 | } 14 | 15 | // Call this method whenever you receive an operation from a client. 16 | Server.prototype.receiveOperation = function (revision, operation) { 17 | if (revision < 0 || this.operations.length < revision) { 18 | throw new Error("operation revision not in history"); 19 | } 20 | // Find all operations that the client didn't know of when it sent the 21 | // operation ... 22 | var concurrentOperations = this.operations.slice(revision); 23 | 24 | // ... and transform the operation against all these operations ... 25 | var transform = operation.constructor.transform; 26 | for (var i = 0; i < concurrentOperations.length; i++) { 27 | operation = transform(operation, concurrentOperations[i])[0]; 28 | } 29 | 30 | // ... and apply that on the document. 31 | this.document = operation.apply(this.document); 32 | // Store operation in history. 33 | this.operations.push(operation); 34 | 35 | // It's the caller's responsibility to send the operation to all connected 36 | // clients and an acknowledgement to the creator. 37 | return operation; 38 | }; 39 | 40 | return Server; 41 | 42 | }(this)); 43 | 44 | if (typeof module === 'object') { 45 | module.exports = ot.Server; 46 | } -------------------------------------------------------------------------------- /ot.js/socketio-adapter.js: -------------------------------------------------------------------------------- 1 | /*global ot */ 2 | 3 | ot.SocketIOAdapter = (function () { 4 | 'use strict'; 5 | 6 | function SocketIOAdapter (socket) { 7 | this.socket = socket; 8 | 9 | var self = this; 10 | socket 11 | .on('client_left', function (clientId) { 12 | self.trigger('client_left', clientId); 13 | }) 14 | .on('set_name', function (clientId, name) { 15 | self.trigger('set_name', clientId, name); 16 | }) 17 | .on('ack', function () { self.trigger('ack'); }) 18 | .on('operation', function (clientId, operation, selection) { 19 | self.trigger('operation', operation); 20 | self.trigger('selection', clientId, selection); 21 | }) 22 | .on('selection', function (clientId, selection) { 23 | self.trigger('selection', clientId, selection); 24 | }) 25 | .on('reconnect', function () { 26 | self.trigger('reconnect'); 27 | }); 28 | } 29 | 30 | SocketIOAdapter.prototype.sendOperation = function (revision, operation, selection) { 31 | this.socket.emit('operation', revision, operation, selection); 32 | }; 33 | 34 | SocketIOAdapter.prototype.sendSelection = function (selection) { 35 | this.socket.emit('selection', selection); 36 | }; 37 | 38 | SocketIOAdapter.prototype.registerCallbacks = function (cb) { 39 | this.callbacks = cb; 40 | }; 41 | 42 | SocketIOAdapter.prototype.trigger = function (event) { 43 | var args = Array.prototype.slice.call(arguments, 1); 44 | var action = this.callbacks && this.callbacks[event]; 45 | if (action) { action.apply(this, args); } 46 | }; 47 | 48 | return SocketIOAdapter; 49 | 50 | }()); -------------------------------------------------------------------------------- /ot.js/wrapped-operation.js: -------------------------------------------------------------------------------- 1 | if (typeof ot === 'undefined') { 2 | // Export for browsers 3 | var ot = {}; 4 | } 5 | 6 | ot.WrappedOperation = (function (global) { 7 | 'use strict'; 8 | 9 | // A WrappedOperation contains an operation and corresponing metadata. 10 | function WrappedOperation (operation, meta) { 11 | this.wrapped = operation; 12 | this.meta = meta; 13 | } 14 | 15 | WrappedOperation.prototype.apply = function () { 16 | return this.wrapped.apply.apply(this.wrapped, arguments); 17 | }; 18 | 19 | WrappedOperation.prototype.invert = function () { 20 | var meta = this.meta; 21 | return new WrappedOperation( 22 | this.wrapped.invert.apply(this.wrapped, arguments), 23 | meta && typeof meta === 'object' && typeof meta.invert === 'function' ? 24 | meta.invert.apply(meta, arguments) : meta 25 | ); 26 | }; 27 | 28 | // Copy all properties from source to target. 29 | function copy (source, target) { 30 | for (var key in source) { 31 | if (source.hasOwnProperty(key)) { 32 | target[key] = source[key]; 33 | } 34 | } 35 | } 36 | 37 | function composeMeta (a, b) { 38 | if (a && typeof a === 'object') { 39 | if (typeof a.compose === 'function') { return a.compose(b); } 40 | var meta = {}; 41 | copy(a, meta); 42 | copy(b, meta); 43 | return meta; 44 | } 45 | return b; 46 | } 47 | 48 | WrappedOperation.prototype.compose = function (other) { 49 | return new WrappedOperation( 50 | this.wrapped.compose(other.wrapped), 51 | composeMeta(this.meta, other.meta) 52 | ); 53 | }; 54 | 55 | function transformMeta (meta, operation) { 56 | if (meta && typeof meta === 'object') { 57 | if (typeof meta.transform === 'function') { 58 | return meta.transform(operation); 59 | } 60 | } 61 | return meta; 62 | } 63 | 64 | WrappedOperation.transform = function (a, b) { 65 | var transform = a.wrapped.constructor.transform; 66 | var pair = transform(a.wrapped, b.wrapped); 67 | return [ 68 | new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)), 69 | new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped)) 70 | ]; 71 | }; 72 | 73 | return WrappedOperation; 74 | 75 | }(this)); 76 | 77 | // Export for CommonJS 78 | if (typeof module === 'object') { 79 | module.exports = ot.WrappedOperation; 80 | } -------------------------------------------------------------------------------- /ot.js/ajax-adapter.js: -------------------------------------------------------------------------------- 1 | /*global ot, $ */ 2 | 3 | ot.AjaxAdapter = (function () { 4 | 'use strict'; 5 | 6 | function AjaxAdapter (path, ownUserName, revision) { 7 | if (path[path.length - 1] !== '/') { path += '/'; } 8 | this.path = path; 9 | this.ownUserName = ownUserName; 10 | this.majorRevision = revision.major || 0; 11 | this.minorRevision = revision.minor || 0; 12 | this.poll(); 13 | } 14 | 15 | AjaxAdapter.prototype.renderRevisionPath = function () { 16 | return 'revision/' + this.majorRevision + '-' + this.minorRevision; 17 | }; 18 | 19 | AjaxAdapter.prototype.handleResponse = function (data) { 20 | var i; 21 | var operations = data.operations; 22 | for (i = 0; i < operations.length; i++) { 23 | if (operations[i].user === this.ownUserName) { 24 | this.trigger('ack'); 25 | } else { 26 | this.trigger('operation', operations[i].operation); 27 | } 28 | } 29 | if (operations.length > 0) { 30 | this.majorRevision += operations.length; 31 | this.minorRevision = 0; 32 | } 33 | 34 | var events = data.events; 35 | if (events) { 36 | for (i = 0; i < events.length; i++) { 37 | var user = events[i].user; 38 | if (user === this.ownUserName) { continue; } 39 | switch (events[i].event) { 40 | case 'joined': this.trigger('set_name', user, user); break; 41 | case 'left': this.trigger('client_left', user); break; 42 | case 'selection': this.trigger('selection', user, events[i].selection); break; 43 | } 44 | } 45 | this.minorRevision += events.length; 46 | } 47 | 48 | var users = data.users; 49 | if (users) { 50 | delete users[this.ownUserName]; 51 | this.trigger('clients', users); 52 | } 53 | 54 | if (data.revision) { 55 | this.majorRevision = data.revision.major; 56 | this.minorRevision = data.revision.minor; 57 | } 58 | }; 59 | 60 | AjaxAdapter.prototype.poll = function () { 61 | var self = this; 62 | $.ajax({ 63 | url: this.path + this.renderRevisionPath(), 64 | type: 'GET', 65 | dataType: 'json', 66 | timeout: 5000, 67 | success: function (data) { 68 | self.handleResponse(data); 69 | self.poll(); 70 | }, 71 | error: function () { 72 | setTimeout(function () { self.poll(); }, 500); 73 | } 74 | }); 75 | }; 76 | 77 | AjaxAdapter.prototype.sendOperation = function (revision, operation, selection) { 78 | if (revision !== this.majorRevision) { throw new Error("Revision numbers out of sync"); } 79 | var self = this; 80 | $.ajax({ 81 | url: this.path + this.renderRevisionPath(), 82 | type: 'POST', 83 | data: JSON.stringify({ operation: operation, selection: selection }), 84 | contentType: 'application/json', 85 | processData: false, 86 | success: function (data) {}, 87 | error: function () { 88 | setTimeout(function () { self.sendOperation(revision, operation, selection); }, 500); 89 | } 90 | }); 91 | }; 92 | 93 | AjaxAdapter.prototype.sendSelection = function (obj) { 94 | $.ajax({ 95 | url: this.path + this.renderRevisionPath() + '/selection', 96 | type: 'POST', 97 | data: JSON.stringify(obj), 98 | contentType: 'application/json', 99 | processData: false, 100 | timeout: 1000 101 | }); 102 | }; 103 | 104 | AjaxAdapter.prototype.registerCallbacks = function (cb) { 105 | this.callbacks = cb; 106 | }; 107 | 108 | AjaxAdapter.prototype.trigger = function (event) { 109 | var args = Array.prototype.slice.call(arguments, 1); 110 | var action = this.callbacks && this.callbacks[event]; 111 | if (action) { action.apply(this, args); } 112 | }; 113 | 114 | return AjaxAdapter; 115 | 116 | })(); -------------------------------------------------------------------------------- /ot.js/selection.js: -------------------------------------------------------------------------------- 1 | if (typeof ot === 'undefined') { 2 | // Export for browsers 3 | var ot = {}; 4 | } 5 | 6 | ot.Selection = (function (global) { 7 | 'use strict'; 8 | 9 | var TextOperation = global.ot ? global.ot.TextOperation : require('./text-operation'); 10 | 11 | // Range has `anchor` and `head` properties, which are zero-based indices into 12 | // the document. The `anchor` is the side of the selection that stays fixed, 13 | // `head` is the side of the selection where the cursor is. When both are 14 | // equal, the range represents a cursor. 15 | function Range (anchor, head) { 16 | this.anchor = anchor; 17 | this.head = head; 18 | } 19 | 20 | Range.fromJSON = function (obj) { 21 | return new Range(obj.anchor, obj.head); 22 | }; 23 | 24 | Range.prototype.equals = function (other) { 25 | return this.anchor === other.anchor && this.head === other.head; 26 | }; 27 | 28 | Range.prototype.isEmpty = function () { 29 | return this.anchor === this.head; 30 | }; 31 | 32 | Range.prototype.transform = function (other) { 33 | function transformIndex (index) { 34 | var newIndex = index; 35 | var ops = other.ops; 36 | for (var i = 0, l = other.ops.length; i < l; i++) { 37 | if (TextOperation.isRetain(ops[i])) { 38 | index -= ops[i]; 39 | } else if (TextOperation.isInsert(ops[i])) { 40 | newIndex += ops[i].length; 41 | } else { 42 | newIndex -= Math.min(index, -ops[i]); 43 | index += ops[i]; 44 | } 45 | if (index < 0) { break; } 46 | } 47 | return newIndex; 48 | } 49 | 50 | var newAnchor = transformIndex(this.anchor); 51 | if (this.anchor === this.head) { 52 | return new Range(newAnchor, newAnchor); 53 | } 54 | return new Range(newAnchor, transformIndex(this.head)); 55 | }; 56 | 57 | // A selection is basically an array of ranges. Every range represents a real 58 | // selection or a cursor in the document (when the start position equals the 59 | // end position of the range). The array must not be empty. 60 | function Selection (ranges) { 61 | this.ranges = ranges || []; 62 | } 63 | 64 | Selection.Range = Range; 65 | 66 | // Convenience method for creating selections only containing a single cursor 67 | // and no real selection range. 68 | Selection.createCursor = function (position) { 69 | return new Selection([new Range(position, position)]); 70 | }; 71 | 72 | Selection.fromJSON = function (obj) { 73 | var objRanges = obj.ranges || obj; 74 | for (var i = 0, ranges = []; i < objRanges.length; i++) { 75 | ranges[i] = Range.fromJSON(objRanges[i]); 76 | } 77 | return new Selection(ranges); 78 | }; 79 | 80 | Selection.prototype.equals = function (other) { 81 | if (this.position !== other.position) { return false; } 82 | if (this.ranges.length !== other.ranges.length) { return false; } 83 | // FIXME: Sort ranges before comparing them? 84 | for (var i = 0; i < this.ranges.length; i++) { 85 | if (!this.ranges[i].equals(other.ranges[i])) { return false; } 86 | } 87 | return true; 88 | }; 89 | 90 | Selection.prototype.somethingSelected = function () { 91 | for (var i = 0; i < this.ranges.length; i++) { 92 | if (!this.ranges[i].isEmpty()) { return true; } 93 | } 94 | return false; 95 | }; 96 | 97 | // Return the more current selection information. 98 | Selection.prototype.compose = function (other) { 99 | return other; 100 | }; 101 | 102 | // Update the selection with respect to an operation. 103 | Selection.prototype.transform = function (other) { 104 | for (var i = 0, newRanges = []; i < this.ranges.length; i++) { 105 | newRanges[i] = this.ranges[i].transform(other); 106 | } 107 | return new Selection(newRanges); 108 | }; 109 | 110 | return Selection; 111 | 112 | }(this)); 113 | 114 | // Export for CommonJS 115 | if (typeof module === 'object') { 116 | module.exports = ot.Selection; 117 | } 118 | -------------------------------------------------------------------------------- /ot.js/undo-manager.js: -------------------------------------------------------------------------------- 1 | if (typeof ot === 'undefined') { 2 | // Export for browsers 3 | var ot = {}; 4 | } 5 | 6 | ot.UndoManager = (function () { 7 | 'use strict'; 8 | 9 | var NORMAL_STATE = 'normal'; 10 | var UNDOING_STATE = 'undoing'; 11 | var REDOING_STATE = 'redoing'; 12 | 13 | // Create a new UndoManager with an optional maximum history size. 14 | function UndoManager (maxItems) { 15 | this.maxItems = maxItems || 50; 16 | this.state = NORMAL_STATE; 17 | this.dontCompose = false; 18 | this.undoStack = []; 19 | this.redoStack = []; 20 | } 21 | 22 | // Add an operation to the undo or redo stack, depending on the current state 23 | // of the UndoManager. The operation added must be the inverse of the last 24 | // edit. When `compose` is true, compose the operation with the last operation 25 | // unless the last operation was alread pushed on the redo stack or was hidden 26 | // by a newer operation on the undo stack. 27 | UndoManager.prototype.add = function (operation, compose) { 28 | if (this.state === UNDOING_STATE) { 29 | this.redoStack.push(operation); 30 | this.dontCompose = true; 31 | } else if (this.state === REDOING_STATE) { 32 | this.undoStack.push(operation); 33 | this.dontCompose = true; 34 | } else { 35 | var undoStack = this.undoStack; 36 | if (!this.dontCompose && compose && undoStack.length > 0) { 37 | undoStack.push(operation.compose(undoStack.pop())); 38 | } else { 39 | undoStack.push(operation); 40 | if (undoStack.length > this.maxItems) { undoStack.shift(); } 41 | } 42 | this.dontCompose = false; 43 | this.redoStack = []; 44 | } 45 | }; 46 | 47 | function transformStack (stack, operation) { 48 | var newStack = []; 49 | var Operation = operation.constructor; 50 | for (var i = stack.length - 1; i >= 0; i--) { 51 | var pair = Operation.transform(stack[i], operation); 52 | if (typeof pair[0].isNoop !== 'function' || !pair[0].isNoop()) { 53 | newStack.push(pair[0]); 54 | } 55 | operation = pair[1]; 56 | } 57 | return newStack.reverse(); 58 | } 59 | 60 | // Transform the undo and redo stacks against a operation by another client. 61 | UndoManager.prototype.transform = function (operation) { 62 | this.undoStack = transformStack(this.undoStack, operation); 63 | this.redoStack = transformStack(this.redoStack, operation); 64 | }; 65 | 66 | // Perform an undo by calling a function with the latest operation on the undo 67 | // stack. The function is expected to call the `add` method with the inverse 68 | // of the operation, which pushes the inverse on the redo stack. 69 | UndoManager.prototype.performUndo = function (fn) { 70 | this.state = UNDOING_STATE; 71 | if (this.undoStack.length === 0) { throw new Error("undo not possible"); } 72 | fn(this.undoStack.pop()); 73 | this.state = NORMAL_STATE; 74 | }; 75 | 76 | // The inverse of `performUndo`. 77 | UndoManager.prototype.performRedo = function (fn) { 78 | this.state = REDOING_STATE; 79 | if (this.redoStack.length === 0) { throw new Error("redo not possible"); } 80 | fn(this.redoStack.pop()); 81 | this.state = NORMAL_STATE; 82 | }; 83 | 84 | // Is the undo stack not empty? 85 | UndoManager.prototype.canUndo = function () { 86 | return this.undoStack.length !== 0; 87 | }; 88 | 89 | // Is the redo stack not empty? 90 | UndoManager.prototype.canRedo = function () { 91 | return this.redoStack.length !== 0; 92 | }; 93 | 94 | // Whether the UndoManager is currently performing an undo. 95 | UndoManager.prototype.isUndoing = function () { 96 | return this.state === UNDOING_STATE; 97 | }; 98 | 99 | // Whether the UndoManager is currently performing a redo. 100 | UndoManager.prototype.isRedoing = function () { 101 | return this.state === REDOING_STATE; 102 | }; 103 | 104 | return UndoManager; 105 | 106 | }()); 107 | 108 | // Export for CommonJS 109 | if (typeof module === 'object') { 110 | module.exports = ot.UndoManager; 111 | } 112 | -------------------------------------------------------------------------------- /ot.js/editor-socketio-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | var TextOperation = require('./text-operation'); 5 | var WrappedOperation = require('./wrapped-operation'); 6 | var Server = require('./server'); 7 | var Selection = require('./selection'); 8 | var util = require('util'); 9 | 10 | function EditorSocketIOServer (document, operations, docId, mayWrite) { 11 | EventEmitter.call(this); 12 | Server.call(this, document, operations); 13 | this.users = {}; 14 | this.docId = docId; 15 | this.mayWrite = mayWrite || function (_, cb) { cb(true); }; 16 | } 17 | 18 | util.inherits(EditorSocketIOServer, Server); 19 | extend(EditorSocketIOServer.prototype, EventEmitter.prototype); 20 | 21 | function extend (target, source) { 22 | for (var key in source) { 23 | if (source.hasOwnProperty(key)) { 24 | target[key] = source[key]; 25 | } 26 | } 27 | } 28 | 29 | EditorSocketIOServer.prototype.addClient = function (socket) { 30 | var self = this; 31 | socket 32 | .join(this.docId) 33 | .emit('doc', { 34 | str: this.document, 35 | revision: this.operations.length, 36 | clients: this.users 37 | }) 38 | .on('operation', function (revision, operation, selection) { 39 | self.mayWrite(socket, function (mayWrite) { 40 | if (!mayWrite) { 41 | console.log("User doesn't have the right to edit."); 42 | return; 43 | } 44 | self.onOperation(socket, revision, operation, selection); 45 | }); 46 | }) 47 | .on('selection', function (obj) { 48 | self.mayWrite(socket, function (mayWrite) { 49 | if (!mayWrite) { 50 | console.log("User doesn't have the right to edit."); 51 | return; 52 | } 53 | self.updateSelection(socket, obj && Selection.fromJSON(obj)); 54 | }); 55 | }) 56 | .on('disconnect', function () { 57 | console.log("Disconnect"); 58 | socket.leave(self.docId); 59 | self.onDisconnect(socket); 60 | if ( 61 | (socket.manager && socket.manager.sockets.clients(self.docId).length === 0) || // socket.io <= 0.9 62 | (socket.ns && Object.keys(socket.ns.connected).length === 0) // socket.io >= 1.0 63 | ) { 64 | self.emit('empty-room'); 65 | } 66 | }); 67 | }; 68 | 69 | EditorSocketIOServer.prototype.onOperation = function (socket, revision, operation, selection) { 70 | var wrapped; 71 | try { 72 | wrapped = new WrappedOperation( 73 | TextOperation.fromJSON(operation), 74 | selection && Selection.fromJSON(selection) 75 | ); 76 | } catch (exc) { 77 | console.error("Invalid operation received: " + exc); 78 | return; 79 | } 80 | 81 | try { 82 | var clientId = socket.id; 83 | var wrappedPrime = this.receiveOperation(revision, wrapped); 84 | console.log("new operation: " + wrapped); 85 | this.getClient(clientId).selection = wrappedPrime.meta; 86 | socket.emit('ack'); 87 | socket.broadcast['in'](this.docId).emit( 88 | 'operation', clientId, 89 | wrappedPrime.wrapped.toJSON(), wrappedPrime.meta 90 | ); 91 | } catch (exc) { 92 | console.error(exc); 93 | } 94 | }; 95 | 96 | EditorSocketIOServer.prototype.updateSelection = function (socket, selection) { 97 | var clientId = socket.id; 98 | if (selection) { 99 | this.getClient(clientId).selection = selection; 100 | } else { 101 | delete this.getClient(clientId).selection; 102 | } 103 | socket.broadcast['in'](this.docId).emit('selection', clientId, selection); 104 | }; 105 | 106 | EditorSocketIOServer.prototype.setName = function (socket, name) { 107 | var clientId = socket.id; 108 | this.getClient(clientId).name = name; 109 | socket.broadcast['in'](this.docId).emit('set_name', clientId, name); 110 | }; 111 | 112 | EditorSocketIOServer.prototype.getClient = function (clientId) { 113 | return this.users[clientId] || (this.users[clientId] = {}); 114 | }; 115 | 116 | EditorSocketIOServer.prototype.onDisconnect = function (socket) { 117 | var clientId = socket.id; 118 | delete this.users[clientId]; 119 | socket.broadcast['in'](this.docId).emit('client_left', clientId); 120 | }; 121 | 122 | module.exports = EditorSocketIOServer; 123 | -------------------------------------------------------------------------------- /ot.js/simple-text-operation.js: -------------------------------------------------------------------------------- 1 | if (typeof ot === 'undefined') { 2 | // Export for browsers 3 | var ot = {}; 4 | } 5 | 6 | ot.SimpleTextOperation = (function (global) { 7 | 8 | var TextOperation = global.ot ? global.ot.TextOperation : require('./text-operation'); 9 | 10 | function SimpleTextOperation () {} 11 | 12 | 13 | // Insert the string `str` at the zero-based `position` in the document. 14 | function Insert (str, position) { 15 | if (!this || this.constructor !== SimpleTextOperation) { 16 | // => function was called without 'new' 17 | return new Insert(str, position); 18 | } 19 | this.str = str; 20 | this.position = position; 21 | } 22 | 23 | Insert.prototype = new SimpleTextOperation(); 24 | SimpleTextOperation.Insert = Insert; 25 | 26 | Insert.prototype.toString = function () { 27 | return 'Insert(' + JSON.stringify(this.str) + ', ' + this.position + ')'; 28 | }; 29 | 30 | Insert.prototype.equals = function (other) { 31 | return other instanceof Insert && 32 | this.str === other.str && 33 | this.position === other.position; 34 | }; 35 | 36 | Insert.prototype.apply = function (doc) { 37 | return doc.slice(0, this.position) + this.str + doc.slice(this.position); 38 | }; 39 | 40 | 41 | // Delete `count` many characters at the zero-based `position` in the document. 42 | function Delete (count, position) { 43 | if (!this || this.constructor !== SimpleTextOperation) { 44 | return new Delete(count, position); 45 | } 46 | this.count = count; 47 | this.position = position; 48 | } 49 | 50 | Delete.prototype = new SimpleTextOperation(); 51 | SimpleTextOperation.Delete = Delete; 52 | 53 | Delete.prototype.toString = function () { 54 | return 'Delete(' + this.count + ', ' + this.position + ')'; 55 | }; 56 | 57 | Delete.prototype.equals = function (other) { 58 | return other instanceof Delete && 59 | this.count === other.count && 60 | this.position === other.position; 61 | }; 62 | 63 | Delete.prototype.apply = function (doc) { 64 | return doc.slice(0, this.position) + doc.slice(this.position + this.count); 65 | }; 66 | 67 | 68 | // An operation that does nothing. This is needed for the result of the 69 | // transformation of two deletions of the same character. 70 | function Noop () { 71 | if (!this || this.constructor !== SimpleTextOperation) { return new Noop(); } 72 | } 73 | 74 | Noop.prototype = new SimpleTextOperation(); 75 | SimpleTextOperation.Noop = Noop; 76 | 77 | Noop.prototype.toString = function () { 78 | return 'Noop()'; 79 | }; 80 | 81 | Noop.prototype.equals = function (other) { return other instanceof Noop; }; 82 | 83 | Noop.prototype.apply = function (doc) { return doc; }; 84 | 85 | var noop = new Noop(); 86 | 87 | 88 | SimpleTextOperation.transform = function (a, b) { 89 | if (a instanceof Noop || b instanceof Noop) { return [a, b]; } 90 | 91 | if (a instanceof Insert && b instanceof Insert) { 92 | if (a.position < b.position || (a.position === b.position && a.str < b.str)) { 93 | return [a, new Insert(b.str, b.position + a.str.length)]; 94 | } 95 | if (a.position > b.position || (a.position === b.position && a.str > b.str)) { 96 | return [new Insert(a.str, a.position + b.str.length), b]; 97 | } 98 | return [noop, noop]; 99 | } 100 | 101 | if (a instanceof Insert && b instanceof Delete) { 102 | if (a.position <= b.position) { 103 | return [a, new Delete(b.count, b.position + a.str.length)]; 104 | } 105 | if (a.position >= b.position + b.count) { 106 | return [new Insert(a.str, a.position - b.count), b]; 107 | } 108 | // Here, we have to delete the inserted string of operation a. 109 | // That doesn't preserve the intention of operation a, but it's the only 110 | // thing we can do to get a valid transform function. 111 | return [noop, new Delete(b.count + a.str.length, b.position)]; 112 | } 113 | 114 | if (a instanceof Delete && b instanceof Insert) { 115 | if (a.position >= b.position) { 116 | return [new Delete(a.count, a.position + b.str.length), b]; 117 | } 118 | if (a.position + a.count <= b.position) { 119 | return [a, new Insert(b.str, b.position - a.count)]; 120 | } 121 | // Same problem as above. We have to delete the string that was inserted 122 | // in operation b. 123 | return [new Delete(a.count + b.str.length, a.position), noop]; 124 | } 125 | 126 | if (a instanceof Delete && b instanceof Delete) { 127 | if (a.position === b.position) { 128 | if (a.count === b.count) { 129 | return [noop, noop]; 130 | } else if (a.count < b.count) { 131 | return [noop, new Delete(b.count - a.count, b.position)]; 132 | } 133 | return [new Delete(a.count - b.count, a.position), noop]; 134 | } 135 | if (a.position < b.position) { 136 | if (a.position + a.count <= b.position) { 137 | return [a, new Delete(b.count, b.position - a.count)]; 138 | } 139 | if (a.position + a.count >= b.position + b.count) { 140 | return [new Delete(a.count - b.count, a.position), noop]; 141 | } 142 | return [ 143 | new Delete(b.position - a.position, a.position), 144 | new Delete(b.position + b.count - (a.position + a.count), a.position) 145 | ]; 146 | } 147 | if (a.position > b.position) { 148 | if (a.position >= b.position + b.count) { 149 | return [new Delete(a.count, a.position - b.count), b]; 150 | } 151 | if (a.position + a.count <= b.position + b.count) { 152 | return [noop, new Delete(b.count - a.count, b.position)]; 153 | } 154 | return [ 155 | new Delete(a.position + a.count - (b.position + b.count), b.position), 156 | new Delete(a.position - b.position, b.position) 157 | ]; 158 | } 159 | } 160 | }; 161 | 162 | // Convert a normal, composable `TextOperation` into an array of 163 | // `SimpleTextOperation`s. 164 | SimpleTextOperation.fromTextOperation = function (operation) { 165 | var simpleOperations = []; 166 | var index = 0; 167 | for (var i = 0; i < operation.ops.length; i++) { 168 | var op = operation.ops[i]; 169 | if (TextOperation.isRetain(op)) { 170 | index += op; 171 | } else if (TextOperation.isInsert(op)) { 172 | simpleOperations.push(new Insert(op, index)); 173 | index += op.length; 174 | } else { 175 | simpleOperations.push(new Delete(Math.abs(op), index)); 176 | } 177 | } 178 | return simpleOperations; 179 | }; 180 | 181 | 182 | return SimpleTextOperation; 183 | })(this); 184 | 185 | // Export for CommonJS 186 | if (typeof module === 'object') { 187 | module.exports = ot.SimpleTextOperation; 188 | } -------------------------------------------------------------------------------- /ot.js/client.js: -------------------------------------------------------------------------------- 1 | // translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala 2 | 3 | if (typeof ot === 'undefined') { 4 | var ot = {}; 5 | } 6 | 7 | ot.Client = (function (global) { 8 | 'use strict'; 9 | 10 | // Client constructor 11 | function Client (revision) { 12 | this.revision = revision; // the next expected revision number 13 | this.state = synchronized_; // start state 14 | } 15 | 16 | Client.prototype.setState = function (state) { 17 | this.state = state; 18 | }; 19 | 20 | // Call this method when the user changes the document. 21 | Client.prototype.applyClient = function (operation) { 22 | this.setState(this.state.applyClient(this, operation)); 23 | }; 24 | 25 | // Call this method with a new operation from the server 26 | Client.prototype.applyServer = function (operation) { 27 | this.revision++; 28 | this.setState(this.state.applyServer(this, operation)); 29 | }; 30 | 31 | Client.prototype.serverAck = function () { 32 | this.revision++; 33 | this.setState(this.state.serverAck(this)); 34 | }; 35 | 36 | Client.prototype.serverReconnect = function () { 37 | if (typeof this.state.resend === 'function') { this.state.resend(this); } 38 | }; 39 | 40 | // Transforms a selection from the latest known server state to the current 41 | // client state. For example, if we get from the server the information that 42 | // another user's cursor is at position 3, but the server hasn't yet received 43 | // our newest operation, an insertion of 5 characters at the beginning of the 44 | // document, the correct position of the other user's cursor in our current 45 | // document is 8. 46 | Client.prototype.transformSelection = function (selection) { 47 | return this.state.transformSelection(selection); 48 | }; 49 | 50 | // Override this method. 51 | Client.prototype.sendOperation = function (revision, operation) { 52 | throw new Error("sendOperation must be defined in child class"); 53 | }; 54 | 55 | // Override this method. 56 | Client.prototype.applyOperation = function (operation) { 57 | throw new Error("applyOperation must be defined in child class"); 58 | }; 59 | 60 | 61 | // In the 'Synchronized' state, there is no pending operation that the client 62 | // has sent to the server. 63 | function Synchronized () {} 64 | Client.Synchronized = Synchronized; 65 | 66 | Synchronized.prototype.applyClient = function (client, operation) { 67 | // When the user makes an edit, send the operation to the server and 68 | // switch to the 'AwaitingConfirm' state 69 | client.sendOperation(client.revision, operation); 70 | return new AwaitingConfirm(operation); 71 | }; 72 | 73 | Synchronized.prototype.applyServer = function (client, operation) { 74 | // When we receive a new operation from the server, the operation can be 75 | // simply applied to the current document 76 | client.applyOperation(operation); 77 | return this; 78 | }; 79 | 80 | Synchronized.prototype.serverAck = function (client) { 81 | throw new Error("There is no pending operation."); 82 | }; 83 | 84 | // Nothing to do because the latest server state and client state are the same. 85 | Synchronized.prototype.transformSelection = function (x) { return x; }; 86 | 87 | // Singleton 88 | var synchronized_ = new Synchronized(); 89 | 90 | 91 | // In the 'AwaitingConfirm' state, there's one operation the client has sent 92 | // to the server and is still waiting for an acknowledgement. 93 | function AwaitingConfirm (outstanding) { 94 | // Save the pending operation 95 | this.outstanding = outstanding; 96 | } 97 | Client.AwaitingConfirm = AwaitingConfirm; 98 | 99 | AwaitingConfirm.prototype.applyClient = function (client, operation) { 100 | // When the user makes an edit, don't send the operation immediately, 101 | // instead switch to 'AwaitingWithBuffer' state 102 | return new AwaitingWithBuffer(this.outstanding, operation); 103 | }; 104 | 105 | AwaitingConfirm.prototype.applyServer = function (client, operation) { 106 | // This is another client's operation. Visualization: 107 | // 108 | // /\ 109 | // this.outstanding / \ operation 110 | // / \ 111 | // \ / 112 | // pair[1] \ / pair[0] (new outstanding) 113 | // (can be applied \/ 114 | // to the client's 115 | // current document) 116 | var pair = operation.constructor.transform(this.outstanding, operation); 117 | client.applyOperation(pair[1]); 118 | return new AwaitingConfirm(pair[0]); 119 | }; 120 | 121 | AwaitingConfirm.prototype.serverAck = function (client) { 122 | // The client's operation has been acknowledged 123 | // => switch to synchronized state 124 | return synchronized_; 125 | }; 126 | 127 | AwaitingConfirm.prototype.transformSelection = function (selection) { 128 | return selection.transform(this.outstanding); 129 | }; 130 | 131 | AwaitingConfirm.prototype.resend = function (client) { 132 | // The confirm didn't come because the client was disconnected. 133 | // Now that it has reconnected, we resend the outstanding operation. 134 | client.sendOperation(client.revision, this.outstanding); 135 | }; 136 | 137 | 138 | // In the 'AwaitingWithBuffer' state, the client is waiting for an operation 139 | // to be acknowledged by the server while buffering the edits the user makes 140 | function AwaitingWithBuffer (outstanding, buffer) { 141 | // Save the pending operation and the user's edits since then 142 | this.outstanding = outstanding; 143 | this.buffer = buffer; 144 | } 145 | Client.AwaitingWithBuffer = AwaitingWithBuffer; 146 | 147 | AwaitingWithBuffer.prototype.applyClient = function (client, operation) { 148 | // Compose the user's changes onto the buffer 149 | var newBuffer = this.buffer.compose(operation); 150 | return new AwaitingWithBuffer(this.outstanding, newBuffer); 151 | }; 152 | 153 | AwaitingWithBuffer.prototype.applyServer = function (client, operation) { 154 | // Operation comes from another client 155 | // 156 | // /\ 157 | // this.outstanding / \ operation 158 | // / \ 159 | // /\ / 160 | // this.buffer / \* / pair1[0] (new outstanding) 161 | // / \/ 162 | // \ / 163 | // pair2[1] \ / pair2[0] (new buffer) 164 | // the transformed \/ 165 | // operation -- can 166 | // be applied to the 167 | // client's current 168 | // document 169 | // 170 | // * pair1[1] 171 | var transform = operation.constructor.transform; 172 | var pair1 = transform(this.outstanding, operation); 173 | var pair2 = transform(this.buffer, pair1[1]); 174 | client.applyOperation(pair2[1]); 175 | return new AwaitingWithBuffer(pair1[0], pair2[0]); 176 | }; 177 | 178 | AwaitingWithBuffer.prototype.serverAck = function (client) { 179 | // The pending operation has been acknowledged 180 | // => send buffer 181 | client.sendOperation(client.revision, this.buffer); 182 | return new AwaitingConfirm(this.buffer); 183 | }; 184 | 185 | AwaitingWithBuffer.prototype.transformSelection = function (selection) { 186 | return selection.transform(this.outstanding).transform(this.buffer); 187 | }; 188 | 189 | AwaitingWithBuffer.prototype.resend = function (client) { 190 | // The confirm didn't come because the client was disconnected. 191 | // Now that it has reconnected, we resend the outstanding operation. 192 | client.sendOperation(client.revision, this.outstanding); 193 | }; 194 | 195 | 196 | return Client; 197 | 198 | }(this)); 199 | 200 | if (typeof module === 'object') { 201 | module.exports = ot.Client; 202 | } 203 | -------------------------------------------------------------------------------- /ot.js/editor-client.js: -------------------------------------------------------------------------------- 1 | /*global ot */ 2 | 3 | ot.EditorClient = (function () { 4 | 'use strict'; 5 | 6 | var Client = ot.Client; 7 | var Selection = ot.Selection; 8 | var UndoManager = ot.UndoManager; 9 | var TextOperation = ot.TextOperation; 10 | var WrappedOperation = ot.WrappedOperation; 11 | 12 | 13 | function SelfMeta (selectionBefore, selectionAfter) { 14 | this.selectionBefore = selectionBefore; 15 | this.selectionAfter = selectionAfter; 16 | } 17 | 18 | SelfMeta.prototype.invert = function () { 19 | return new SelfMeta(this.selectionAfter, this.selectionBefore); 20 | }; 21 | 22 | SelfMeta.prototype.compose = function (other) { 23 | return new SelfMeta(this.selectionBefore, other.selectionAfter); 24 | }; 25 | 26 | SelfMeta.prototype.transform = function (operation) { 27 | return new SelfMeta( 28 | this.selectionBefore.transform(operation), 29 | this.selectionAfter.transform(operation) 30 | ); 31 | }; 32 | 33 | 34 | function OtherMeta (clientId, selection) { 35 | this.clientId = clientId; 36 | this.selection = selection; 37 | } 38 | 39 | OtherMeta.fromJSON = function (obj) { 40 | return new OtherMeta( 41 | obj.clientId, 42 | obj.selection && Selection.fromJSON(obj.selection) 43 | ); 44 | }; 45 | 46 | OtherMeta.prototype.transform = function (operation) { 47 | return new OtherMeta( 48 | this.clientId, 49 | this.selection && this.selection.transform(operation) 50 | ); 51 | }; 52 | 53 | 54 | function OtherClient (id, listEl, editorAdapter, name, selection) { 55 | this.id = id; 56 | this.listEl = listEl; 57 | this.editorAdapter = editorAdapter; 58 | this.name = name; 59 | 60 | this.li = document.createElement('li'); 61 | if (name) { 62 | this.li.textContent = name; 63 | this.listEl.appendChild(this.li); 64 | } 65 | 66 | this.setColor(name ? hueFromName(name) : Math.random()); 67 | if (selection) { this.updateSelection(selection); } 68 | } 69 | 70 | OtherClient.prototype.setColor = function (hue) { 71 | this.hue = hue; 72 | this.color = hsl2hex(hue, 0.75, 0.5); 73 | this.lightColor = hsl2hex(hue, 0.5, 0.9); 74 | if (this.li) { this.li.style.color = this.color; } 75 | }; 76 | 77 | OtherClient.prototype.setName = function (name) { 78 | if (this.name === name) { return; } 79 | this.name = name; 80 | 81 | this.li.textContent = name; 82 | if (!this.li.parentNode) { 83 | this.listEl.appendChild(this.li); 84 | } 85 | 86 | this.setColor(hueFromName(name)); 87 | }; 88 | 89 | OtherClient.prototype.updateSelection = function (selection) { 90 | this.removeSelection(); 91 | this.selection = selection; 92 | this.mark = this.editorAdapter.setOtherSelection( 93 | selection, 94 | selection.position === selection.selectionEnd ? this.color : this.lightColor, 95 | this.id 96 | ); 97 | }; 98 | 99 | OtherClient.prototype.remove = function () { 100 | if (this.li) { removeElement(this.li); } 101 | this.removeSelection(); 102 | }; 103 | 104 | OtherClient.prototype.removeSelection = function () { 105 | if (this.mark) { 106 | this.mark.clear(); 107 | this.mark = null; 108 | } 109 | }; 110 | 111 | 112 | function EditorClient (revision, clients, serverAdapter, editorAdapter) { 113 | Client.call(this, revision); 114 | this.serverAdapter = serverAdapter; 115 | this.editorAdapter = editorAdapter; 116 | this.undoManager = new UndoManager(); 117 | 118 | this.initializeClientList(); 119 | this.initializeClients(clients); 120 | 121 | var self = this; 122 | 123 | this.editorAdapter.registerCallbacks({ 124 | change: function (operation, inverse) { self.onChange(operation, inverse); }, 125 | selectionChange: function () { self.onSelectionChange(); }, 126 | blur: function () { self.onBlur(); } 127 | }); 128 | this.editorAdapter.registerUndo(function () { self.undo(); }); 129 | this.editorAdapter.registerRedo(function () { self.redo(); }); 130 | 131 | this.serverAdapter.registerCallbacks({ 132 | client_left: function (clientId) { self.onClientLeft(clientId); }, 133 | set_name: function (clientId, name) { self.getClientObject(clientId).setName(name); }, 134 | ack: function () { self.serverAck(); }, 135 | operation: function (operation) { 136 | self.applyServer(TextOperation.fromJSON(operation)); 137 | }, 138 | selection: function (clientId, selection) { 139 | if (selection) { 140 | self.getClientObject(clientId).updateSelection( 141 | self.transformSelection(Selection.fromJSON(selection)) 142 | ); 143 | } else { 144 | self.getClientObject(clientId).removeSelection(); 145 | } 146 | }, 147 | clients: function (clients) { 148 | var clientId; 149 | for (clientId in self.clients) { 150 | if (self.clients.hasOwnProperty(clientId) && !clients.hasOwnProperty(clientId)) { 151 | self.onClientLeft(clientId); 152 | } 153 | } 154 | 155 | for (clientId in clients) { 156 | if (clients.hasOwnProperty(clientId)) { 157 | var clientObject = self.getClientObject(clientId); 158 | 159 | if (clients[clientId].name) { 160 | clientObject.setName(clients[clientId].name); 161 | } 162 | 163 | var selection = clients[clientId].selection; 164 | if (selection) { 165 | self.clients[clientId].updateSelection( 166 | self.transformSelection(Selection.fromJSON(selection)) 167 | ); 168 | } else { 169 | self.clients[clientId].removeSelection(); 170 | } 171 | } 172 | } 173 | }, 174 | reconnect: function () { self.serverReconnect(); } 175 | }); 176 | } 177 | 178 | inherit(EditorClient, Client); 179 | 180 | EditorClient.prototype.addClient = function (clientId, clientObj) { 181 | this.clients[clientId] = new OtherClient( 182 | clientId, 183 | this.clientListEl, 184 | this.editorAdapter, 185 | clientObj.name || clientId, 186 | clientObj.selection ? Selection.fromJSON(clientObj.selection) : null 187 | ); 188 | }; 189 | 190 | EditorClient.prototype.initializeClients = function (clients) { 191 | this.clients = {}; 192 | for (var clientId in clients) { 193 | if (clients.hasOwnProperty(clientId)) { 194 | this.addClient(clientId, clients[clientId]); 195 | } 196 | } 197 | }; 198 | 199 | EditorClient.prototype.getClientObject = function (clientId) { 200 | var client = this.clients[clientId]; 201 | if (client) { return client; } 202 | return this.clients[clientId] = new OtherClient( 203 | clientId, 204 | this.clientListEl, 205 | this.editorAdapter 206 | ); 207 | }; 208 | 209 | EditorClient.prototype.onClientLeft = function (clientId) { 210 | console.log("User disconnected: " + clientId); 211 | var client = this.clients[clientId]; 212 | if (!client) { return; } 213 | client.remove(); 214 | delete this.clients[clientId]; 215 | }; 216 | 217 | EditorClient.prototype.initializeClientList = function () { 218 | this.clientListEl = document.createElement('ul'); 219 | }; 220 | 221 | EditorClient.prototype.applyUnredo = function (operation) { 222 | this.undoManager.add(operation.invert(this.editorAdapter.getValue())); 223 | this.editorAdapter.applyOperation(operation.wrapped); 224 | this.selection = operation.meta.selectionAfter; 225 | this.editorAdapter.setSelection(this.selection); 226 | this.applyClient(operation.wrapped); 227 | }; 228 | 229 | EditorClient.prototype.undo = function () { 230 | var self = this; 231 | if (!this.undoManager.canUndo()) { return; } 232 | this.undoManager.performUndo(function (o) { self.applyUnredo(o); }); 233 | }; 234 | 235 | EditorClient.prototype.redo = function () { 236 | var self = this; 237 | if (!this.undoManager.canRedo()) { return; } 238 | this.undoManager.performRedo(function (o) { self.applyUnredo(o); }); 239 | }; 240 | 241 | EditorClient.prototype.onChange = function (textOperation, inverse) { 242 | var selectionBefore = this.selection; 243 | this.updateSelection(); 244 | var meta = new SelfMeta(selectionBefore, this.selection); 245 | var operation = new WrappedOperation(textOperation, meta); 246 | 247 | var compose = this.undoManager.undoStack.length > 0 && 248 | inverse.shouldBeComposedWithInverted(last(this.undoManager.undoStack).wrapped); 249 | var inverseMeta = new SelfMeta(this.selection, selectionBefore); 250 | this.undoManager.add(new WrappedOperation(inverse, inverseMeta), compose); 251 | this.applyClient(textOperation); 252 | }; 253 | 254 | EditorClient.prototype.updateSelection = function () { 255 | this.selection = this.editorAdapter.getSelection(); 256 | }; 257 | 258 | EditorClient.prototype.onSelectionChange = function () { 259 | var oldSelection = this.selection; 260 | this.updateSelection(); 261 | if (oldSelection && this.selection.equals(oldSelection)) { return; } 262 | this.sendSelection(this.selection); 263 | }; 264 | 265 | EditorClient.prototype.onBlur = function () { 266 | this.selection = null; 267 | this.sendSelection(null); 268 | }; 269 | 270 | EditorClient.prototype.sendSelection = function (selection) { 271 | if (this.state instanceof Client.AwaitingWithBuffer) { return; } 272 | this.serverAdapter.sendSelection(selection); 273 | }; 274 | 275 | EditorClient.prototype.sendOperation = function (revision, operation) { 276 | this.serverAdapter.sendOperation(revision, operation.toJSON(), this.selection); 277 | }; 278 | 279 | EditorClient.prototype.applyOperation = function (operation) { 280 | this.editorAdapter.applyOperation(operation); 281 | this.updateSelection(); 282 | this.undoManager.transform(new WrappedOperation(operation, null)); 283 | }; 284 | 285 | function rgb2hex (r, g, b) { 286 | function digits (n) { 287 | var m = Math.round(255*n).toString(16); 288 | return m.length === 1 ? '0'+m : m; 289 | } 290 | return '#' + digits(r) + digits(g) + digits(b); 291 | } 292 | 293 | function hsl2hex (h, s, l) { 294 | if (s === 0) { return rgb2hex(l, l, l); } 295 | var var2 = l < 0.5 ? l * (1+s) : (l+s) - (s*l); 296 | var var1 = 2 * l - var2; 297 | var hue2rgb = function (hue) { 298 | if (hue < 0) { hue += 1; } 299 | if (hue > 1) { hue -= 1; } 300 | if (6*hue < 1) { return var1 + (var2-var1)*6*hue; } 301 | if (2*hue < 1) { return var2; } 302 | if (3*hue < 2) { return var1 + (var2-var1)*6*(2/3 - hue); } 303 | return var1; 304 | }; 305 | return rgb2hex(hue2rgb(h+1/3), hue2rgb(h), hue2rgb(h-1/3)); 306 | } 307 | 308 | function hueFromName (name) { 309 | var a = 1; 310 | for (var i = 0; i < name.length; i++) { 311 | a = 17 * (a+name.charCodeAt(i)) % 360; 312 | } 313 | return a/360; 314 | } 315 | 316 | // Set Const.prototype.__proto__ to Super.prototype 317 | function inherit (Const, Super) { 318 | function F () {} 319 | F.prototype = Super.prototype; 320 | Const.prototype = new F(); 321 | Const.prototype.constructor = Const; 322 | } 323 | 324 | function last (arr) { return arr[arr.length - 1]; } 325 | 326 | // Remove an element from the DOM. 327 | function removeElement (el) { 328 | if (el.parentNode) { 329 | el.parentNode.removeChild(el); 330 | } 331 | } 332 | 333 | return EditorClient; 334 | }()); 335 | -------------------------------------------------------------------------------- /ot.js/codemirror-adapter.js: -------------------------------------------------------------------------------- 1 | /*global ot */ 2 | 3 | ot.CodeMirrorAdapter = (function (global) { 4 | 'use strict'; 5 | 6 | var TextOperation = ot.TextOperation; 7 | var Selection = ot.Selection; 8 | 9 | function CodeMirrorAdapter (cm) { 10 | this.cm = cm; 11 | this.ignoreNextChange = false; 12 | this.changeInProgress = false; 13 | this.selectionChanged = false; 14 | 15 | bind(this, 'onChanges'); 16 | bind(this, 'onChange'); 17 | bind(this, 'onCursorActivity'); 18 | bind(this, 'onFocus'); 19 | bind(this, 'onBlur'); 20 | 21 | cm.on('changes', this.onChanges); 22 | cm.on('change', this.onChange); 23 | cm.on('cursorActivity', this.onCursorActivity); 24 | cm.on('focus', this.onFocus); 25 | cm.on('blur', this.onBlur); 26 | } 27 | 28 | // Removes all event listeners from the CodeMirror instance. 29 | CodeMirrorAdapter.prototype.detach = function () { 30 | this.cm.off('changes', this.onChanges); 31 | this.cm.off('change', this.onChange); 32 | this.cm.off('cursorActivity', this.onCursorActivity); 33 | this.cm.off('focus', this.onFocus); 34 | this.cm.off('blur', this.onBlur); 35 | }; 36 | 37 | function cmpPos (a, b) { 38 | if (a.line < b.line) { return -1; } 39 | if (a.line > b.line) { return 1; } 40 | if (a.ch < b.ch) { return -1; } 41 | if (a.ch > b.ch) { return 1; } 42 | return 0; 43 | } 44 | function posEq (a, b) { return cmpPos(a, b) === 0; } 45 | function posLe (a, b) { return cmpPos(a, b) <= 0; } 46 | 47 | function minPos (a, b) { return posLe(a, b) ? a : b; } 48 | function maxPos (a, b) { return posLe(a, b) ? b : a; } 49 | 50 | function codemirrorDocLength (doc) { 51 | return doc.indexFromPos({ line: doc.lastLine(), ch: 0 }) + 52 | doc.getLine(doc.lastLine()).length; 53 | } 54 | 55 | // Converts a CodeMirror change array (as obtained from the 'changes' event 56 | // in CodeMirror v4) or single change or linked list of changes (as returned 57 | // by the 'change' event in CodeMirror prior to version 4) into a 58 | // TextOperation and its inverse and returns them as a two-element array. 59 | CodeMirrorAdapter.operationFromCodeMirrorChanges = function (changes, doc) { 60 | // Approach: Replay the changes, beginning with the most recent one, and 61 | // construct the operation and its inverse. We have to convert the position 62 | // in the pre-change coordinate system to an index. We have a method to 63 | // convert a position in the coordinate system after all changes to an index, 64 | // namely CodeMirror's `indexFromPos` method. We can use the information of 65 | // a single change object to convert a post-change coordinate system to a 66 | // pre-change coordinate system. We can now proceed inductively to get a 67 | // pre-change coordinate system for all changes in the linked list. 68 | // A disadvantage of this approach is its complexity `O(n^2)` in the length 69 | // of the linked list of changes. 70 | 71 | var docEndLength = codemirrorDocLength(doc); 72 | var operation = new TextOperation().retain(docEndLength); 73 | var inverse = new TextOperation().retain(docEndLength); 74 | 75 | var indexFromPos = function (pos) { 76 | return doc.indexFromPos(pos); 77 | }; 78 | 79 | function last (arr) { return arr[arr.length - 1]; } 80 | 81 | function sumLengths (strArr) { 82 | if (strArr.length === 0) { return 0; } 83 | var sum = 0; 84 | for (var i = 0; i < strArr.length; i++) { sum += strArr[i].length; } 85 | return sum + strArr.length - 1; 86 | } 87 | 88 | function updateIndexFromPos (indexFromPos, change) { 89 | return function (pos) { 90 | if (posLe(pos, change.from)) { return indexFromPos(pos); } 91 | if (posLe(change.to, pos)) { 92 | return indexFromPos({ 93 | line: pos.line + change.text.length - 1 - (change.to.line - change.from.line), 94 | ch: (change.to.line < pos.line) ? 95 | pos.ch : 96 | (change.text.length <= 1) ? 97 | pos.ch - (change.to.ch - change.from.ch) + sumLengths(change.text) : 98 | pos.ch - change.to.ch + last(change.text).length 99 | }) + sumLengths(change.removed) - sumLengths(change.text); 100 | } 101 | if (change.from.line === pos.line) { 102 | return indexFromPos(change.from) + pos.ch - change.from.ch; 103 | } 104 | return indexFromPos(change.from) + 105 | sumLengths(change.removed.slice(0, pos.line - change.from.line)) + 106 | 1 + pos.ch; 107 | }; 108 | } 109 | 110 | for (var i = changes.length - 1; i >= 0; i--) { 111 | var change = changes[i]; 112 | indexFromPos = updateIndexFromPos(indexFromPos, change); 113 | 114 | var fromIndex = indexFromPos(change.from); 115 | var restLength = docEndLength - fromIndex - sumLengths(change.text); 116 | 117 | operation = new TextOperation() 118 | .retain(fromIndex) 119 | ['delete'](sumLengths(change.removed)) 120 | .insert(change.text.join('\n')) 121 | .retain(restLength) 122 | .compose(operation); 123 | 124 | inverse = inverse.compose(new TextOperation() 125 | .retain(fromIndex) 126 | ['delete'](sumLengths(change.text)) 127 | .insert(change.removed.join('\n')) 128 | .retain(restLength) 129 | ); 130 | 131 | docEndLength += sumLengths(change.removed) - sumLengths(change.text); 132 | } 133 | 134 | return [operation, inverse]; 135 | }; 136 | 137 | // Singular form for backwards compatibility. 138 | CodeMirrorAdapter.operationFromCodeMirrorChange = 139 | CodeMirrorAdapter.operationFromCodeMirrorChanges; 140 | 141 | // Apply an operation to a CodeMirror instance. 142 | CodeMirrorAdapter.applyOperationToCodeMirror = function (operation, cm) { 143 | cm.operation(function () { 144 | var ops = operation.ops; 145 | var index = 0; // holds the current index into CodeMirror's content 146 | for (var i = 0, l = ops.length; i < l; i++) { 147 | var op = ops[i]; 148 | if (TextOperation.isRetain(op)) { 149 | index += op; 150 | } else if (TextOperation.isInsert(op)) { 151 | cm.replaceRange(op, cm.posFromIndex(index)); 152 | index += op.length; 153 | } else if (TextOperation.isDelete(op)) { 154 | var from = cm.posFromIndex(index); 155 | var to = cm.posFromIndex(index - op); 156 | cm.replaceRange('', from, to); 157 | } 158 | } 159 | }); 160 | }; 161 | 162 | CodeMirrorAdapter.prototype.registerCallbacks = function (cb) { 163 | this.callbacks = cb; 164 | }; 165 | 166 | CodeMirrorAdapter.prototype.onChange = function () { 167 | // By default, CodeMirror's event order is the following: 168 | // 1. 'change', 2. 'cursorActivity', 3. 'changes'. 169 | // We want to fire the 'selectionChange' event after the 'change' event, 170 | // but need the information from the 'changes' event. Therefore, we detect 171 | // when a change is in progress by listening to the change event, setting 172 | // a flag that makes this adapter defer all 'cursorActivity' events. 173 | this.changeInProgress = true; 174 | }; 175 | 176 | CodeMirrorAdapter.prototype.onChanges = function (_, changes) { 177 | if (!this.ignoreNextChange) { 178 | var pair = CodeMirrorAdapter.operationFromCodeMirrorChanges(changes, this.cm); 179 | this.trigger('change', pair[0], pair[1]); 180 | } 181 | if (this.selectionChanged) { this.trigger('selectionChange'); } 182 | this.changeInProgress = false; 183 | this.ignoreNextChange = false; 184 | }; 185 | 186 | CodeMirrorAdapter.prototype.onCursorActivity = 187 | CodeMirrorAdapter.prototype.onFocus = function () { 188 | if (this.changeInProgress) { 189 | this.selectionChanged = true; 190 | } else { 191 | this.trigger('selectionChange'); 192 | } 193 | }; 194 | 195 | CodeMirrorAdapter.prototype.onBlur = function () { 196 | if (!this.cm.somethingSelected()) { this.trigger('blur'); } 197 | }; 198 | 199 | CodeMirrorAdapter.prototype.getValue = function () { 200 | return this.cm.getValue(); 201 | }; 202 | 203 | CodeMirrorAdapter.prototype.getSelection = function () { 204 | var cm = this.cm; 205 | 206 | var selectionList = cm.listSelections(); 207 | var ranges = []; 208 | for (var i = 0; i < selectionList.length; i++) { 209 | ranges[i] = new Selection.Range( 210 | cm.indexFromPos(selectionList[i].anchor), 211 | cm.indexFromPos(selectionList[i].head) 212 | ); 213 | } 214 | 215 | return new Selection(ranges); 216 | }; 217 | 218 | CodeMirrorAdapter.prototype.setSelection = function (selection) { 219 | var ranges = []; 220 | for (var i = 0; i < selection.ranges.length; i++) { 221 | var range = selection.ranges[i]; 222 | ranges[i] = { 223 | anchor: this.cm.posFromIndex(range.anchor), 224 | head: this.cm.posFromIndex(range.head) 225 | }; 226 | } 227 | this.cm.setSelections(ranges); 228 | }; 229 | 230 | var addStyleRule = (function () { 231 | var added = {}; 232 | var styleElement = document.createElement('style'); 233 | document.documentElement.getElementsByTagName('head')[0].appendChild(styleElement); 234 | var styleSheet = styleElement.sheet; 235 | 236 | return function (css) { 237 | if (added[css]) { return; } 238 | added[css] = true; 239 | styleSheet.insertRule(css, (styleSheet.cssRules || styleSheet.rules).length); 240 | }; 241 | }()); 242 | 243 | CodeMirrorAdapter.prototype.setOtherCursor = function (position, color, clientId) { 244 | var cursorPos = this.cm.posFromIndex(position); 245 | var cursorCoords = this.cm.cursorCoords(cursorPos); 246 | var cursorEl = document.createElement('span'); 247 | cursorEl.className = 'other-client'; 248 | cursorEl.style.display = 'inline-block'; 249 | cursorEl.style.padding = '0'; 250 | cursorEl.style.marginLeft = cursorEl.style.marginRight = '-1px'; 251 | cursorEl.style.borderLeftWidth = '2px'; 252 | cursorEl.style.borderLeftStyle = 'solid'; 253 | cursorEl.style.borderLeftColor = color; 254 | cursorEl.style.height = (cursorCoords.bottom - cursorCoords.top) * 0.9 + 'px'; 255 | cursorEl.style.zIndex = 0; 256 | cursorEl.setAttribute('data-clientid', clientId); 257 | return this.cm.setBookmark(cursorPos, { widget: cursorEl, insertLeft: true }); 258 | }; 259 | 260 | CodeMirrorAdapter.prototype.setOtherSelectionRange = function (range, color, clientId) { 261 | var match = /^#([0-9a-fA-F]{6})$/.exec(color); 262 | if (!match) { throw new Error("only six-digit hex colors are allowed."); } 263 | var selectionClassName = 'selection-' + match[1]; 264 | var rule = '.' + selectionClassName + ' { background: ' + color + '; }'; 265 | addStyleRule(rule); 266 | 267 | var anchorPos = this.cm.posFromIndex(range.anchor); 268 | var headPos = this.cm.posFromIndex(range.head); 269 | 270 | return this.cm.markText( 271 | minPos(anchorPos, headPos), 272 | maxPos(anchorPos, headPos), 273 | { className: selectionClassName } 274 | ); 275 | }; 276 | 277 | CodeMirrorAdapter.prototype.setOtherSelection = function (selection, color, clientId) { 278 | var selectionObjects = []; 279 | for (var i = 0; i < selection.ranges.length; i++) { 280 | var range = selection.ranges[i]; 281 | if (range.isEmpty()) { 282 | selectionObjects[i] = this.setOtherCursor(range.head, color, clientId); 283 | } else { 284 | selectionObjects[i] = this.setOtherSelectionRange(range, color, clientId); 285 | } 286 | } 287 | return { 288 | clear: function () { 289 | for (var i = 0; i < selectionObjects.length; i++) { 290 | selectionObjects[i].clear(); 291 | } 292 | } 293 | }; 294 | }; 295 | 296 | CodeMirrorAdapter.prototype.trigger = function (event) { 297 | var args = Array.prototype.slice.call(arguments, 1); 298 | var action = this.callbacks && this.callbacks[event]; 299 | if (action) { action.apply(this, args); } 300 | }; 301 | 302 | CodeMirrorAdapter.prototype.applyOperation = function (operation) { 303 | this.ignoreNextChange = true; 304 | CodeMirrorAdapter.applyOperationToCodeMirror(operation, this.cm); 305 | }; 306 | 307 | CodeMirrorAdapter.prototype.registerUndo = function (undoFn) { 308 | this.cm.undo = undoFn; 309 | }; 310 | 311 | CodeMirrorAdapter.prototype.registerRedo = function (redoFn) { 312 | this.cm.redo = redoFn; 313 | }; 314 | 315 | // Throws an error if the first argument is falsy. Useful for debugging. 316 | function assert (b, msg) { 317 | if (!b) { 318 | throw new Error(msg || "assertion error"); 319 | } 320 | } 321 | 322 | // Bind a method to an object, so it doesn't matter whether you call 323 | // object.method() directly or pass object.method as a reference to another 324 | // function. 325 | function bind (obj, method) { 326 | var fn = obj[method]; 327 | obj[method] = function () { 328 | fn.apply(obj, arguments); 329 | }; 330 | } 331 | 332 | return CodeMirrorAdapter; 333 | 334 | }(this)); 335 | -------------------------------------------------------------------------------- /ot.js/text-operation.js: -------------------------------------------------------------------------------- 1 | if (typeof ot === 'undefined') { 2 | // Export for browsers 3 | var ot = {}; 4 | } 5 | 6 | ot.TextOperation = (function () { 7 | 'use strict'; 8 | 9 | // Constructor for new operations. 10 | function TextOperation () { 11 | if (!this || this.constructor !== TextOperation) { 12 | // => function was called without 'new' 13 | return new TextOperation(); 14 | } 15 | 16 | // When an operation is applied to an input string, you can think of this as 17 | // if an imaginary cursor runs over the entire string and skips over some 18 | // parts, deletes some parts and inserts characters at some positions. These 19 | // actions (skip/delete/insert) are stored as an array in the "ops" property. 20 | this.ops = []; 21 | // An operation's baseLength is the length of every string the operation 22 | // can be applied to. 23 | this.baseLength = 0; 24 | // The targetLength is the length of every string that results from applying 25 | // the operation on a valid input string. 26 | this.targetLength = 0; 27 | } 28 | 29 | TextOperation.prototype.equals = function (other) { 30 | if (this.baseLength !== other.baseLength) { return false; } 31 | if (this.targetLength !== other.targetLength) { return false; } 32 | if (this.ops.length !== other.ops.length) { return false; } 33 | for (var i = 0; i < this.ops.length; i++) { 34 | if (this.ops[i] !== other.ops[i]) { return false; } 35 | } 36 | return true; 37 | }; 38 | 39 | // Operation are essentially lists of ops. There are three types of ops: 40 | // 41 | // * Retain ops: Advance the cursor position by a given number of characters. 42 | // Represented by positive ints. 43 | // * Insert ops: Insert a given string at the current cursor position. 44 | // Represented by strings. 45 | // * Delete ops: Delete the next n characters. Represented by negative ints. 46 | 47 | var isRetain = TextOperation.isRetain = function (op) { 48 | return typeof op === 'number' && op > 0; 49 | }; 50 | 51 | var isInsert = TextOperation.isInsert = function (op) { 52 | return typeof op === 'string'; 53 | }; 54 | 55 | var isDelete = TextOperation.isDelete = function (op) { 56 | return typeof op === 'number' && op < 0; 57 | }; 58 | 59 | 60 | // After an operation is constructed, the user of the library can specify the 61 | // actions of an operation (skip/insert/delete) with these three builder 62 | // methods. They all return the operation for convenient chaining. 63 | 64 | // Skip over a given number of characters. 65 | TextOperation.prototype.retain = function (n) { 66 | if (typeof n !== 'number') { 67 | throw new Error("retain expects an integer"); 68 | } 69 | if (n === 0) { return this; } 70 | this.baseLength += n; 71 | this.targetLength += n; 72 | if (isRetain(this.ops[this.ops.length-1])) { 73 | // The last op is a retain op => we can merge them into one op. 74 | this.ops[this.ops.length-1] += n; 75 | } else { 76 | // Create a new op. 77 | this.ops.push(n); 78 | } 79 | return this; 80 | }; 81 | 82 | // Insert a string at the current position. 83 | TextOperation.prototype.insert = function (str) { 84 | if (typeof str !== 'string') { 85 | throw new Error("insert expects a string"); 86 | } 87 | if (str === '') { return this; } 88 | this.targetLength += str.length; 89 | var ops = this.ops; 90 | if (isInsert(ops[ops.length-1])) { 91 | // Merge insert op. 92 | ops[ops.length-1] += str; 93 | } else if (isDelete(ops[ops.length-1])) { 94 | // It doesn't matter when an operation is applied whether the operation 95 | // is delete(3), insert("something") or insert("something"), delete(3). 96 | // Here we enforce that in this case, the insert op always comes first. 97 | // This makes all operations that have the same effect when applied to 98 | // a document of the right length equal in respect to the `equals` method. 99 | if (isInsert(ops[ops.length-2])) { 100 | ops[ops.length-2] += str; 101 | } else { 102 | ops[ops.length] = ops[ops.length-1]; 103 | ops[ops.length-2] = str; 104 | } 105 | } else { 106 | ops.push(str); 107 | } 108 | return this; 109 | }; 110 | 111 | // Delete a string at the current position. 112 | TextOperation.prototype['delete'] = function (n) { 113 | if (typeof n === 'string') { n = n.length; } 114 | if (typeof n !== 'number') { 115 | throw new Error("delete expects an integer or a string"); 116 | } 117 | if (n === 0) { return this; } 118 | if (n > 0) { n = -n; } 119 | this.baseLength -= n; 120 | if (isDelete(this.ops[this.ops.length-1])) { 121 | this.ops[this.ops.length-1] += n; 122 | } else { 123 | this.ops.push(n); 124 | } 125 | return this; 126 | }; 127 | 128 | // Tests whether this operation has no effect. 129 | TextOperation.prototype.isNoop = function () { 130 | return this.ops.length === 0 || (this.ops.length === 1 && isRetain(this.ops[0])); 131 | }; 132 | 133 | // Pretty printing. 134 | TextOperation.prototype.toString = function () { 135 | // map: build a new array by applying a function to every element in an old 136 | // array. 137 | var map = Array.prototype.map || function (fn) { 138 | var arr = this; 139 | var newArr = []; 140 | for (var i = 0, l = arr.length; i < l; i++) { 141 | newArr[i] = fn(arr[i]); 142 | } 143 | return newArr; 144 | }; 145 | return map.call(this.ops, function (op) { 146 | if (isRetain(op)) { 147 | return "retain " + op; 148 | } else if (isInsert(op)) { 149 | return "insert '" + op + "'"; 150 | } else { 151 | return "delete " + (-op); 152 | } 153 | }).join(', '); 154 | }; 155 | 156 | // Converts operation into a JSON value. 157 | TextOperation.prototype.toJSON = function () { 158 | return this.ops; 159 | }; 160 | 161 | // Converts a plain JS object into an operation and validates it. 162 | TextOperation.fromJSON = function (ops) { 163 | var o = new TextOperation(); 164 | for (var i = 0, l = ops.length; i < l; i++) { 165 | var op = ops[i]; 166 | if (isRetain(op)) { 167 | o.retain(op); 168 | } else if (isInsert(op)) { 169 | o.insert(op); 170 | } else if (isDelete(op)) { 171 | o['delete'](op); 172 | } else { 173 | throw new Error("unknown operation: " + JSON.stringify(op)); 174 | } 175 | } 176 | return o; 177 | }; 178 | 179 | // Apply an operation to a string, returning a new string. Throws an error if 180 | // there's a mismatch between the input string and the operation. 181 | TextOperation.prototype.apply = function (str) { 182 | var operation = this; 183 | if (str.length !== operation.baseLength) { 184 | throw new Error("The operation's base length must be equal to the string's length."); 185 | } 186 | var newStr = [], j = 0; 187 | var strIndex = 0; 188 | var ops = this.ops; 189 | for (var i = 0, l = ops.length; i < l; i++) { 190 | var op = ops[i]; 191 | if (isRetain(op)) { 192 | if (strIndex + op > str.length) { 193 | throw new Error("Operation can't retain more characters than are left in the string."); 194 | } 195 | // Copy skipped part of the old string. 196 | newStr[j++] = str.slice(strIndex, strIndex + op); 197 | strIndex += op; 198 | } else if (isInsert(op)) { 199 | // Insert string. 200 | newStr[j++] = op; 201 | } else { // delete op 202 | strIndex -= op; 203 | } 204 | } 205 | if (strIndex !== str.length) { 206 | throw new Error("The operation didn't operate on the whole string."); 207 | } 208 | return newStr.join(''); 209 | }; 210 | 211 | // Computes the inverse of an operation. The inverse of an operation is the 212 | // operation that reverts the effects of the operation, e.g. when you have an 213 | // operation 'insert("hello "); skip(6);' then the inverse is 'delete("hello "); 214 | // skip(6);'. The inverse should be used for implementing undo. 215 | TextOperation.prototype.invert = function (str) { 216 | var strIndex = 0; 217 | var inverse = new TextOperation(); 218 | var ops = this.ops; 219 | for (var i = 0, l = ops.length; i < l; i++) { 220 | var op = ops[i]; 221 | if (isRetain(op)) { 222 | inverse.retain(op); 223 | strIndex += op; 224 | } else if (isInsert(op)) { 225 | inverse['delete'](op.length); 226 | } else { // delete op 227 | inverse.insert(str.slice(strIndex, strIndex - op)); 228 | strIndex -= op; 229 | } 230 | } 231 | return inverse; 232 | }; 233 | 234 | // Compose merges two consecutive operations into one operation, that 235 | // preserves the changes of both. Or, in other words, for each input string S 236 | // and a pair of consecutive operations A and B, 237 | // apply(apply(S, A), B) = apply(S, compose(A, B)) must hold. 238 | TextOperation.prototype.compose = function (operation2) { 239 | var operation1 = this; 240 | if (operation1.targetLength !== operation2.baseLength) { 241 | throw new Error("The base length of the second operation has to be the target length of the first operation"); 242 | } 243 | 244 | var operation = new TextOperation(); // the combined operation 245 | var ops1 = operation1.ops, ops2 = operation2.ops; // for fast access 246 | var i1 = 0, i2 = 0; // current index into ops1 respectively ops2 247 | var op1 = ops1[i1++], op2 = ops2[i2++]; // current ops 248 | while (true) { 249 | // Dispatch on the type of op1 and op2 250 | if (typeof op1 === 'undefined' && typeof op2 === 'undefined') { 251 | // end condition: both ops1 and ops2 have been processed 252 | break; 253 | } 254 | 255 | if (isDelete(op1)) { 256 | operation['delete'](op1); 257 | op1 = ops1[i1++]; 258 | continue; 259 | } 260 | if (isInsert(op2)) { 261 | operation.insert(op2); 262 | op2 = ops2[i2++]; 263 | continue; 264 | } 265 | 266 | if (typeof op1 === 'undefined') { 267 | throw new Error("Cannot compose operations: first operation is too short."); 268 | } 269 | if (typeof op2 === 'undefined') { 270 | throw new Error("Cannot compose operations: first operation is too long."); 271 | } 272 | 273 | if (isRetain(op1) && isRetain(op2)) { 274 | if (op1 > op2) { 275 | operation.retain(op2); 276 | op1 = op1 - op2; 277 | op2 = ops2[i2++]; 278 | } else if (op1 === op2) { 279 | operation.retain(op1); 280 | op1 = ops1[i1++]; 281 | op2 = ops2[i2++]; 282 | } else { 283 | operation.retain(op1); 284 | op2 = op2 - op1; 285 | op1 = ops1[i1++]; 286 | } 287 | } else if (isInsert(op1) && isDelete(op2)) { 288 | if (op1.length > -op2) { 289 | op1 = op1.slice(-op2); 290 | op2 = ops2[i2++]; 291 | } else if (op1.length === -op2) { 292 | op1 = ops1[i1++]; 293 | op2 = ops2[i2++]; 294 | } else { 295 | op2 = op2 + op1.length; 296 | op1 = ops1[i1++]; 297 | } 298 | } else if (isInsert(op1) && isRetain(op2)) { 299 | if (op1.length > op2) { 300 | operation.insert(op1.slice(0, op2)); 301 | op1 = op1.slice(op2); 302 | op2 = ops2[i2++]; 303 | } else if (op1.length === op2) { 304 | operation.insert(op1); 305 | op1 = ops1[i1++]; 306 | op2 = ops2[i2++]; 307 | } else { 308 | operation.insert(op1); 309 | op2 = op2 - op1.length; 310 | op1 = ops1[i1++]; 311 | } 312 | } else if (isRetain(op1) && isDelete(op2)) { 313 | if (op1 > -op2) { 314 | operation['delete'](op2); 315 | op1 = op1 + op2; 316 | op2 = ops2[i2++]; 317 | } else if (op1 === -op2) { 318 | operation['delete'](op2); 319 | op1 = ops1[i1++]; 320 | op2 = ops2[i2++]; 321 | } else { 322 | operation['delete'](op1); 323 | op2 = op2 + op1; 324 | op1 = ops1[i1++]; 325 | } 326 | } else { 327 | throw new Error( 328 | "This shouldn't happen: op1: " + 329 | JSON.stringify(op1) + ", op2: " + 330 | JSON.stringify(op2) 331 | ); 332 | } 333 | } 334 | return operation; 335 | }; 336 | 337 | function getSimpleOp (operation, fn) { 338 | var ops = operation.ops; 339 | var isRetain = TextOperation.isRetain; 340 | switch (ops.length) { 341 | case 1: 342 | return ops[0]; 343 | case 2: 344 | return isRetain(ops[0]) ? ops[1] : (isRetain(ops[1]) ? ops[0] : null); 345 | case 3: 346 | if (isRetain(ops[0]) && isRetain(ops[2])) { return ops[1]; } 347 | } 348 | return null; 349 | } 350 | 351 | function getStartIndex (operation) { 352 | if (isRetain(operation.ops[0])) { return operation.ops[0]; } 353 | return 0; 354 | } 355 | 356 | // When you use ctrl-z to undo your latest changes, you expect the program not 357 | // to undo every single keystroke but to undo your last sentence you wrote at 358 | // a stretch or the deletion you did by holding the backspace key down. This 359 | // This can be implemented by composing operations on the undo stack. This 360 | // method can help decide whether two operations should be composed. It 361 | // returns true if the operations are consecutive insert operations or both 362 | // operations delete text at the same position. You may want to include other 363 | // factors like the time since the last change in your decision. 364 | TextOperation.prototype.shouldBeComposedWith = function (other) { 365 | if (this.isNoop() || other.isNoop()) { return true; } 366 | 367 | var startA = getStartIndex(this), startB = getStartIndex(other); 368 | var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other); 369 | if (!simpleA || !simpleB) { return false; } 370 | 371 | if (isInsert(simpleA) && isInsert(simpleB)) { 372 | return startA + simpleA.length === startB; 373 | } 374 | 375 | if (isDelete(simpleA) && isDelete(simpleB)) { 376 | // there are two possibilities to delete: with backspace and with the 377 | // delete key. 378 | return (startB - simpleB === startA) || startA === startB; 379 | } 380 | 381 | return false; 382 | }; 383 | 384 | // Decides whether two operations should be composed with each other 385 | // if they were inverted, that is 386 | // `shouldBeComposedWith(a, b) = shouldBeComposedWithInverted(b^{-1}, a^{-1})`. 387 | TextOperation.prototype.shouldBeComposedWithInverted = function (other) { 388 | if (this.isNoop() || other.isNoop()) { return true; } 389 | 390 | var startA = getStartIndex(this), startB = getStartIndex(other); 391 | var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other); 392 | if (!simpleA || !simpleB) { return false; } 393 | 394 | if (isInsert(simpleA) && isInsert(simpleB)) { 395 | return startA + simpleA.length === startB || startA === startB; 396 | } 397 | 398 | if (isDelete(simpleA) && isDelete(simpleB)) { 399 | return startB - simpleB === startA; 400 | } 401 | 402 | return false; 403 | }; 404 | 405 | // Transform takes two operations A and B that happened concurrently and 406 | // produces two operations A' and B' (in an array) such that 407 | // `apply(apply(S, A), B') = apply(apply(S, B), A')`. This function is the 408 | // heart of OT. 409 | TextOperation.transform = function (operation1, operation2) { 410 | if (operation1.baseLength !== operation2.baseLength) { 411 | throw new Error("Both operations have to have the same base length"); 412 | } 413 | 414 | var operation1prime = new TextOperation(); 415 | var operation2prime = new TextOperation(); 416 | var ops1 = operation1.ops, ops2 = operation2.ops; 417 | var i1 = 0, i2 = 0; 418 | var op1 = ops1[i1++], op2 = ops2[i2++]; 419 | while (true) { 420 | // At every iteration of the loop, the imaginary cursor that both 421 | // operation1 and operation2 have that operates on the input string must 422 | // have the same position in the input string. 423 | 424 | if (typeof op1 === 'undefined' && typeof op2 === 'undefined') { 425 | // end condition: both ops1 and ops2 have been processed 426 | break; 427 | } 428 | 429 | // next two cases: one or both ops are insert ops 430 | // => insert the string in the corresponding prime operation, skip it in 431 | // the other one. If both op1 and op2 are insert ops, prefer op1. 432 | if (isInsert(op1)) { 433 | operation1prime.insert(op1); 434 | operation2prime.retain(op1.length); 435 | op1 = ops1[i1++]; 436 | continue; 437 | } 438 | if (isInsert(op2)) { 439 | operation1prime.retain(op2.length); 440 | operation2prime.insert(op2); 441 | op2 = ops2[i2++]; 442 | continue; 443 | } 444 | 445 | if (typeof op1 === 'undefined') { 446 | throw new Error("Cannot compose operations: first operation is too short."); 447 | } 448 | if (typeof op2 === 'undefined') { 449 | throw new Error("Cannot compose operations: first operation is too long."); 450 | } 451 | 452 | var minl; 453 | if (isRetain(op1) && isRetain(op2)) { 454 | // Simple case: retain/retain 455 | if (op1 > op2) { 456 | minl = op2; 457 | op1 = op1 - op2; 458 | op2 = ops2[i2++]; 459 | } else if (op1 === op2) { 460 | minl = op2; 461 | op1 = ops1[i1++]; 462 | op2 = ops2[i2++]; 463 | } else { 464 | minl = op1; 465 | op2 = op2 - op1; 466 | op1 = ops1[i1++]; 467 | } 468 | operation1prime.retain(minl); 469 | operation2prime.retain(minl); 470 | } else if (isDelete(op1) && isDelete(op2)) { 471 | // Both operations delete the same string at the same position. We don't 472 | // need to produce any operations, we just skip over the delete ops and 473 | // handle the case that one operation deletes more than the other. 474 | if (-op1 > -op2) { 475 | op1 = op1 - op2; 476 | op2 = ops2[i2++]; 477 | } else if (op1 === op2) { 478 | op1 = ops1[i1++]; 479 | op2 = ops2[i2++]; 480 | } else { 481 | op2 = op2 - op1; 482 | op1 = ops1[i1++]; 483 | } 484 | // next two cases: delete/retain and retain/delete 485 | } else if (isDelete(op1) && isRetain(op2)) { 486 | if (-op1 > op2) { 487 | minl = op2; 488 | op1 = op1 + op2; 489 | op2 = ops2[i2++]; 490 | } else if (-op1 === op2) { 491 | minl = op2; 492 | op1 = ops1[i1++]; 493 | op2 = ops2[i2++]; 494 | } else { 495 | minl = -op1; 496 | op2 = op2 + op1; 497 | op1 = ops1[i1++]; 498 | } 499 | operation1prime['delete'](minl); 500 | } else if (isRetain(op1) && isDelete(op2)) { 501 | if (op1 > -op2) { 502 | minl = -op2; 503 | op1 = op1 + op2; 504 | op2 = ops2[i2++]; 505 | } else if (op1 === -op2) { 506 | minl = op1; 507 | op1 = ops1[i1++]; 508 | op2 = ops2[i2++]; 509 | } else { 510 | minl = op1; 511 | op2 = op2 + op1; 512 | op1 = ops1[i1++]; 513 | } 514 | operation2prime['delete'](minl); 515 | } else { 516 | throw new Error("The two operations aren't compatible"); 517 | } 518 | } 519 | 520 | return [operation1prime, operation2prime]; 521 | }; 522 | 523 | return TextOperation; 524 | 525 | }()); 526 | 527 | // Export for CommonJS 528 | if (typeof module === 'object') { 529 | module.exports = ot.TextOperation; 530 | } --------------------------------------------------------------------------------