text: (function (global, factory) {
    if (typeof define === "function" && define.amd) {
      define(['module', './Utils.js', './TextAction.js'], factory);
    } else if (typeof exports !== "undefined") {
      factory(module, require('./Utils.js'), require('./TextAction.js'));
    } else {
      var mod = {
        exports: {}
      };
      factory(mod, global.Utils, global.TextAction);
      global.TextOperation = mod.exports;
    }
  })(this, function (module, Utils, TextAction) {
    'use strict';
  
    var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
      return typeof obj;
    } : function (obj) {
      return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
    };
  
    function _classCallCheck(instance, Constructor) {
      if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
      }
    }
  
    var _createClass = function () {
      function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
          var descriptor = props[i];
          descriptor.enumerable = descriptor.enumerable || false;
          descriptor.configurable = true;
          if ("value" in descriptor) descriptor.writable = true;
          Object.defineProperty(target, descriptor.key, descriptor);
        }
      }
  
      return function (Constructor, protoProps, staticProps) {
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
      };
    }();
  
    module.exports = function () {
      function TextOperation() {
        _classCallCheck(this, 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;
      }
  
      _createClass(TextOperation, [{
        key: 'equals',
        value: function equals(other) {
          if (this.baseLength !== other.baseLength || this.targetLength !== other.targetLength || this.ops.length !== other.ops.length) {
            return false;
          }
          for (var i = 0; i < this.ops.length; i++) {
            if (!this.ops[i].equals(other.ops[i])) {
              return false;
            }
          }
          return true;
        }
      }, {
        key: 'retain',
        value: function retain(n, attributes) {
          if (typeof n !== 'number' || n < 0) {
            throw new Error('retain expects a positive integer.');
          }
          if (n === 0) {
            return this;
          }
          this.baseLength += n;
          this.targetLength += n;
          attributes = attributes || {};
          var prevOp = this.ops.length > 0 ? this.ops[this.ops.length - 1] : null;
          if (prevOp && prevOp.isRetain() && prevOp.attributesEqual(attributes)) {
            // The last op is a retain op with the same attributes
            // => so we can merge them into one op.
            prevOp.chars += n;
          } else {
            // Create a new TextAction.
            this.ops.push(new TextAction('retain', n, attributes));
          }
          return this;
        }
      }, {
        key: 'insert',
        value: function insert(str, attributes) {
          if (typeof str !== 'string') {
            throw new Error('insert expects a string');
          }
          if (str === '') {
            return this;
          }
          attributes = attributes || {};
          this.targetLength += str.length;
  
          var prevOp = this.ops.length > 0 ? this.ops[this.ops.length - 1] : null;
          var prevPrevOp = this.ops.length > 1 ? this.ops[this.ops.length - 2] : null;
  
          if (prevOp && prevOp.isInsert() && prevOp.attributesEqual(attributes)) {
            // Merge insert op.
            prevOp.text += str;
          } else if (prevOp && prevOp.isDelete()) {
            // 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 (prevPrevOp && prevPrevOp.isInsert() && prevPrevOp.attributesEqual(attributes)) {
              prevPrevOp.text += str;
            } else {
              this.ops[this.ops.length - 1] = new TextAction('insert', str, attributes);
              this.ops.push(prevOp);
            }
          } else {
            this.ops.push(new TextAction('insert', str, attributes));
          }
          return this;
        }
      }, {
        key: 'delete',
        value: function _delete(n) {
          if (typeof n === 'string') {
            n = n.length;
          }
          if (typeof n !== 'number' || n < 0) {
            throw new Error('delete expects a positive integer or a string');
          }
          if (n === 0) {
            return this;
          }
          this.baseLength += n;
  
          var prevOp = this.ops.length > 0 ? this.ops[this.ops.length - 1] : null;
  
          if (prevOp && prevOp.isDelete()) {
            prevOp.chars += n;
          } else {
            this.ops.push(new TextAction('delete', n));
          }
          return this;
        }
      }, {
        key: 'isNoop',
        value: function isNoop() {
          return this.ops.length === 0 || this.ops.length === 1 && this.ops[0].isRetain() && this.ops[0].hasEmptyAttributes();
        }
      }, {
        key: 'clone',
        value: function clone() {
          var clone = new TextOperation();
          // replay the operation actions
          for (var i = 0; i < this.ops.length; i++) {
            if (this.ops[i].isRetain()) {
              clone.retain(this.ops[i].chars, this.ops[i].attributes);
            } else if (this.ops[i].isInsert()) {
              clone.insert(this.ops[i].text, this.ops[i].attributes);
            } else {
              // delete
              clone.delete(this.ops[i].chars);
            }
          }
          return clone;
        }
      }, {
        key: 'toJSON',
        value: function toJSON() {
          var ops = [];
          var _iteratorNormalCompletion = true;
          var _didIteratorError = false;
          var _iteratorError = undefined;
  
          try {
            for (var _iterator = this.ops[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
              var op = _step.value;
  
              // We pre push ops' attributes if non-empty.
              if (!op.hasEmptyAttributes()) {
                ops.push(op.attributes);
              }
              if (op.isRetain()) {
                ops.push(op.chars); // retain ops using positive ints
              } else if (op.isInsert()) {
                ops.push(op.text);
              } else if (op.isDelete()) {
                ops.push(-op.chars); // delete ops using negative ints
              }
            }
          } catch (err) {
            _didIteratorError = true;
            _iteratorError = err;
          } finally {
            try {
              if (!_iteratorNormalCompletion && _iterator.return) {
                _iterator.return();
              }
            } finally {
              if (_didIteratorError) {
                throw _iteratorError;
              }
            }
          }
  
          return ops;
        }
      }, {
        key: 'apply',
        value: function apply(str, oldAttributes, newAttributes) {
          var operation = this;
          oldAttributes = oldAttributes || [];
          newAttributes = newAttributes || [];
          if (str.length !== operation.baseLength) {
            throw new Error("The operation's base length must be equal to the string's length.");
          }
          var newStringParts = [];
          var j = 0;
          var k;
          var attr;
          var oldIndex = 0;
          var ops = this.ops;
          for (var i = 0, l = ops.length; i < l; i++) {
            var op = ops[i];
            if (op.isRetain()) {
              if (oldIndex + op.chars > str.length) {
                throw new Error("Operation can't retain more characters than are left in the string.");
              }
              // Copy skipped part of the retained string.
              newStringParts[j++] = str.slice(oldIndex, oldIndex + op.chars);
  
              // Copy (and potentially update) attributes for each char in retained string.
              for (k = 0; k < op.chars; k++) {
                var currAttributes = oldAttributes[oldIndex + k] || {};
                var updatedAttributes = {};
                for (attr in currAttributes) {
                  updatedAttributes[attr] = currAttributes[attr];
                  Utils.assert(updatedAttributes[attr] !== false);
                }
                for (attr in op.attributes) {
                  if (op.attributes[attr] === false) {
                    delete updatedAttributes[attr];
                  } else {
                    updatedAttributes[attr] = op.attributes[attr];
                  }
                  Utils.assert(updatedAttributes[attr] !== false);
                }
                newAttributes.push(updatedAttributes);
              }
  
              oldIndex += op.chars;
            } else if (op.isInsert()) {
              // Insert string.
              newStringParts[j++] = op.text;
  
              // Insert attributes for each char.
              for (k = 0; k < op.text.length; k++) {
                var insertedAttributes = {};
                for (attr in op.attributes) {
                  insertedAttributes[attr] = op.attributes[attr];
                  Utils.assert(insertedAttributes[attr] !== false);
                }
                newAttributes.push(insertedAttributes);
              }
            } else {
              // delete op
              oldIndex += op.chars;
            }
          }
          if (oldIndex !== str.length) {
            throw new Error("The operation didn't operate on the whole string.");
          }
          var newString = newStringParts.join('');
          Utils.assert(newString.length === newAttributes.length);
  
          return newString;
        }
      }, {
        key: 'invert',
        value: function invert(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 (op.isRetain()) {
              inverse.retain(op.chars);
              strIndex += op.chars;
            } else if (op.isInsert()) {
              inverse['delete'](op.text.length);
            } else {
              // delete op
              inverse.insert(str.slice(strIndex, strIndex + op.chars));
              strIndex += op.chars;
            }
          }
          return inverse;
        }
      }, {
        key: 'compose',
        value: function compose(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');
          }
  
          function composeAttributes(first, second, firstOpIsInsert) {
            var merged = {};
            var attr;
            for (attr in first) {
              merged[attr] = first[attr];
            }
            for (attr in second) {
              if (firstOpIsInsert && second[attr] === false) {
                delete merged[attr];
              } else {
                merged[attr] = second[attr];
              }
            }
            return merged;
          }
  
          var operation = new TextOperation(); // the combined operation
          var ops1 = operation1.clone().ops;
          var ops2 = operation2.clone().ops;
          // current index into ops1 respectively ops2
          var i1 = 0;
          var i2 = 0;
          var op1 = ops1[i1++];
          var op2 = ops2[i2++]; // current ops
          var attributes;
          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 (op1 && op1.isDelete()) {
              operation.delete(op1.chars);
              op1 = ops1[i1++];
              continue;
            }
            if (op2 && op2.isInsert()) {
              operation.insert(op2.text, op2.attributes);
              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 (op1.isRetain() && op2.isRetain()) {
              attributes = composeAttributes(op1.attributes, op2.attributes);
              if (op1.chars > op2.chars) {
                operation.retain(op2.chars, attributes);
                op1.chars -= op2.chars;
                op2 = ops2[i2++];
              } else if (op1.chars === op2.chars) {
                operation.retain(op1.chars, attributes);
                op1 = ops1[i1++];
                op2 = ops2[i2++];
              } else {
                operation.retain(op1.chars, attributes);
                op2.chars -= op1.chars;
                op1 = ops1[i1++];
              }
            } else if (op1.isInsert() && op2.isDelete()) {
              if (op1.text.length > op2.chars) {
                op1.text = op1.text.slice(op2.chars);
                op2 = ops2[i2++];
              } else if (op1.text.length === op2.chars) {
                op1 = ops1[i1++];
                op2 = ops2[i2++];
              } else {
                op2.chars -= op1.text.length;
                op1 = ops1[i1++];
              }
            } else if (op1.isInsert() && op2.isRetain()) {
              attributes = composeAttributes(op1.attributes, op2.attributes, /* firstOpIsInsert= */true);
              if (op1.text.length > op2.chars) {
                operation.insert(op1.text.slice(0, op2.chars), attributes);
                op1.text = op1.text.slice(op2.chars);
                op2 = ops2[i2++];
              } else if (op1.text.length === op2.chars) {
                operation.insert(op1.text, attributes);
                op1 = ops1[i1++];
                op2 = ops2[i2++];
              } else {
                operation.insert(op1.text, attributes);
                op2.chars -= op1.text.length;
                op1 = ops1[i1++];
              }
            } else if (op1.isRetain() && op2.isDelete()) {
              if (op1.chars > op2.chars) {
                operation['delete'](op2.chars);
                op1.chars -= op2.chars;
                op2 = ops2[i2++];
              } else if (op1.chars === op2.chars) {
                operation['delete'](op2.chars);
                op1 = ops1[i1++];
                op2 = ops2[i2++];
              } else {
                operation['delete'](op1.chars);
                op2.chars -= op1.chars;
                op1 = ops1[i1++];
              }
            } else {
              throw new Error("This shouldn't happen: op1: " + JSON.stringify(op1) + ', op2: ' + JSON.stringify(op2));
            }
          }
          return operation;
        }
      }, {
        key: 'shouldBeComposedWith',
        value: function shouldBeComposedWith(other) {
          if (this.isNoop() || other.isNoop()) {
            return true;
          }
  
          var startA = getStartIndex(this);
          var startB = getStartIndex(other);
          var simpleA = getSimpleOp(this);
          var simpleB = getSimpleOp(other);
          if (!simpleA || !simpleB) {
            return false;
          }
  
          if (simpleA.isInsert() && simpleB.isInsert()) {
            return startA + simpleA.text.length === startB;
          }
  
          if (simpleA.isDelete() && simpleB.isDelete()) {
            // there are two possibilities to delete: with backspace and with the
            // delete key.
            return startB + simpleB.chars === startA || startA === startB;
          }
  
          return false;
        }
      }, {
        key: 'shouldBeComposedWithInverted',
        value: function shouldBeComposedWithInverted(other) {
          if (this.isNoop() || other.isNoop()) {
            return true;
          }
  
          var startA = getStartIndex(this);
          var startB = getStartIndex(other);
          var simpleA = getSimpleOp(this);
          var simpleB = getSimpleOp(other);
          if (!simpleA || !simpleB) {
            return false;
          }
  
          if (simpleA.isInsert() && simpleB.isInsert()) {
            return startA + simpleA.text.length === startB || startA === startB;
          }
  
          if (simpleA.isDelete() && simpleB.isDelete()) {
            return startB + simpleB.chars === startA;
          }
  
          return false;
        }
      }, {
        key: 'transform',
        value: function transform(other) {
          return TextOperation.transform(this, other);
        }
      }, {
        key: 'toString',
        value: function toString() {
          // TODO...
        }
      }], [{
        key: 'fromJSON',
        value: function fromJSON(ops) {
          var textOperation = new TextOperation();
          for (var i = 0; i < ops.length; i++) {
            var op = ops[i];
            var attributes = {};
            if ((typeof op === 'undefined' ? 'undefined' : _typeof(op)) === 'object') {
              attributes = op;
              // both retain and insert operation can has attributes
              i++;
              op = ops[i];
            }
            if (typeof op === 'number') {
              if (op > 0) {
                // retain
                textOperation.retain(op, attributes);
              } else {
                // delete
                textOperation.delete(-op);
              }
            } else {
              Utils.assert(typeof op === 'string');
              textOperation.insert(op, attributes);
            }
          }
  
          return textOperation;
        }
      }, {
        key: 'transformAttributes',
        value: function transformAttributes(attributes1, attributes2) {
          var attributes1prime = {};
          var attributes2prime = {};
          var attr;
          var allAttrs = {};
          for (attr in attributes1) {
            allAttrs[attr] = true;
          }
          for (attr in attributes2) {
            allAttrs[attr] = true;
          }
  
          for (attr in allAttrs) {
            var attr1 = attributes1[attr];
            var attr2 = attributes2[attr];
            Utils.assert(attr1 != null || attr2 != null);
            if (attr1 == null) {
              // Only modified by attributes2; keep it.
              attributes2prime[attr] = attr2;
            } else if (attr2 == null) {
              // only modified by attributes1; keep it
              attributes1prime[attr] = attr1;
            } else if (attr1 === attr2) {
              // Both set it to the same value.  Nothing to do.
            } else {
              // attr1 and attr2 are different. Prefer attr1.
              attributes1prime[attr] = attr1;
            }
          }
          return [attributes1prime, attributes2prime];
        }
      }, {
        key: 'transform',
        value: function transform(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.clone().ops;
          var ops2 = operation2.clone().ops;
          var i1 = 0;
          var i2 = 0;
          var op1 = ops1[i1++];
          var 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 (op1 && op1.isInsert()) {
              operation1prime.insert(op1.text, op1.attributes);
              operation2prime.retain(op1.text.length);
              op1 = ops1[i1++];
              continue;
            }
            if (op2 && op2.isInsert()) {
              operation1prime.retain(op2.text.length);
              operation2prime.insert(op2.text, op2.attributes);
              op2 = ops2[i2++];
              continue;
            }
  
            if (typeof op1 === 'undefined') {
              throw new Error('Cannot transform operations: first operation is too short.');
            }
            if (typeof op2 === 'undefined') {
              throw new Error('Cannot transform operations: first operation is too long.');
            }
  
            var minl;
            if (op1.isRetain() && op2.isRetain()) {
              // Simple case: retain/retain
              var attributesPrime = TextOperation.transformAttributes(op1.attributes, op2.attributes);
              if (op1.chars > op2.chars) {
                minl = op2.chars;
                op1.chars -= op2.chars;
                op2 = ops2[i2++];
              } else if (op1.chars === op2.chars) {
                minl = op2.chars;
                op1 = ops1[i1++];
                op2 = ops2[i2++];
              } else {
                minl = op1.chars;
                op2.chars -= op1.chars;
                op1 = ops1[i1++];
              }
  
              operation1prime.retain(minl, attributesPrime[0]);
              operation2prime.retain(minl, attributesPrime[1]);
            } else if (op1.isDelete() && op2.isDelete()) {
              // 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.chars > op2.chars) {
                op1.chars -= op2.chars;
                op2 = ops2[i2++];
              } else if (op1.chars === op2.chars) {
                op1 = ops1[i1++];
                op2 = ops2[i2++];
              } else {
                op2.chars -= op1.chars;
                op1 = ops1[i1++];
              }
              // next two cases: delete/retain and retain/delete
            } else if (op1.isDelete() && op2.isRetain()) {
              if (op1.chars > op2.chars) {
                minl = op2.chars;
                op1.chars -= op2.chars;
                op2 = ops2[i2++];
              } else if (op1.chars === op2.chars) {
                minl = op2.chars;
                op1 = ops1[i1++];
                op2 = ops2[i2++];
              } else {
                minl = op1.chars;
                op2.chars -= op1.chars;
                op1 = ops1[i1++];
              }
              operation1prime.delete(minl);
            } else if (op1.isRetain() && op2.isDelete()) {
              if (op1.chars > op2.chars) {
                minl = op2.chars;
                op1.chars -= op2.chars;
                op2 = ops2[i2++];
              } else if (op1.chars === op2.chars) {
                minl = op1.chars;
                op1 = ops1[i1++];
                op2 = ops2[i2++];
              } else {
                minl = op1.chars;
                op2.chars -= op1.chars;
                op1 = ops1[i1++];
              }
              operation2prime.delete(minl);
            } else {
              throw new Error("The two operations aren't compatible");
            }
          }
  
          return [operation1prime, operation2prime];
        }
      }]);
  
      return TextOperation;
    }();
  
    // helper method
    function getSimpleOp(operation) {
      var ops = operation.ops;
      switch (ops.length) {
        case 1:
          return ops[0];
        case 2:
          return ops[0].isRetain() ? ops[1] : ops[1].isRetain() ? ops[0] : null;
        case 3:
          if (ops[0].isRetain() && ops[2].isRetain()) {
            return ops[1];
          }
      }
      return null;
    }
  
    function getStartIndex(operation) {
      if (operation.ops[0].isRetain()) {
        return operation.ops[0].chars;
      }
      return 0;
    }
  });