public List<TokenHook> compile() throws InvalidOperationException {
    if (this.compiled) {
      throw new IllegalStateException("XMLEditableString already compiled");
    }

    if (tokenMask != null) {
      for (int[] positions : this.tokenMask) {
        int startPosition = positions[0];
        int length = positions[1];
        TokenHook hook = new TokenHook(startPosition, length, TokenHook.TokenType.Word);
        hook.processedString = this.currentString.substring(startPosition, startPosition + length);
        this.tokens.add(hook);
      }
    }

    this.reverseChangeLog();
    this.compiled = true;

    ArrayList<TokenHook> hooks = new ArrayList<>(this.tokens.size() + this.xml.size());
    hooks.addAll(this.tokens);
    hooks.addAll(this.xml);

    Collections.sort(hooks, (t1, t2) -> t1.startIndex - t2.startIndex);

    return hooks;
  }
  protected void applyOperations(Collection<Operation> operations, boolean save)
      throws InvalidOperationException {
    if (this.compiled) {
      throw new IllegalStateException("XMLEditableString already compiled");
    }
    for (Operation operation : operations) {
      int operationEndIndex = operation.startIndex + operation.length;
      operation.originalString =
          this.currentString.substring(operation.startIndex, operationEndIndex);

      int delta = operation.lengthNewString - operation.length;
      if (delta != 0) {
        for (TokenHook hook : this.tokens) {
          int hookLastEditedIndex = hook.startIndex + hook.length - 1;
          if (hook.startIndex >= operationEndIndex) {
            hook.startIndex += delta;
          } else if (hook.startIndex > operation.startIndex) {
            throw new InvalidOperationException(operation, hook);
          } else if (hook.startIndex == operation.startIndex && operation.length > hook.length) {
            throw new InvalidOperationException(operation, hook);
          } else if (hook.startIndex == operation.startIndex && operation.length < hook.length) {
            hook.length += delta;
          } else if (hook.startIndex == operation.startIndex) {
            hook.length = operation.lengthNewString;
          } else if (hookLastEditedIndex >= operationEndIndex) {
            hook.length += delta;
          } else if (hookLastEditedIndex >= operation.startIndex) {
            throw new InvalidOperationException(operation, hook);
          } else if (hookLastEditedIndex < operation.startIndex) {
            // Do nothing
          } else {
            throw new InvalidOperationException(operation, hook, "Unexpected situation");
          }
        }
      }

      if (TokenHook.TokenType.XML.equals(operation.tokenType)) {
        for (TokenHook hook : this.xml) {
          int hookLastEditedIndex = hook.startIndex + hook.length - 1;
          if (hook.startIndex >= operationEndIndex) {
            hook.startIndex += delta;
          } else if (hook.startIndex > operation.startIndex) {
            throw new InvalidOperationException(operation, hook);
          } else if (hook.startIndex == operation.startIndex && operation.length > hook.length) {
            throw new InvalidOperationException(operation, hook);
          } else if (hook.startIndex == operation.startIndex && operation.length < hook.length) {
            hook.length += delta;
          } else if (hook.startIndex == operation.startIndex) {
            hook.length = operation.lengthNewString;
          } else if (hookLastEditedIndex >= operationEndIndex) {
            hook.length += delta;
          } else if (hookLastEditedIndex >= operation.startIndex) {
            throw new InvalidOperationException(operation, hook);
          } else if (hookLastEditedIndex < operation.startIndex) {
            // Do nothing
          } else {
            throw new InvalidOperationException(operation, hook, "Unexpected situation");
          }
        }
      }

      if (!TokenHook.TokenType.Word.equals(operation.tokenType)) {
        this.currentString.replace(operation.startIndex, operationEndIndex, operation.newString);
      }

      if (save) {
        if (operation.tokenType != null) {
          if (TokenHook.TokenType.XML.equals(operation.tokenType)) {
            TokenHook hook =
                new TokenHook(operation.startIndex, operation.lengthNewString, operation.tokenType);
            this.xml.add(hook);
          } else {
            if (this.tokenMask == null) {
              this.tokenMask = new TokenMask(this.currentString.length());
            }

            this.tokenMask.setToken(operation.startIndex, operation.lengthNewString);
          }
        }

        this.changeLog.push(operation);
      }
    }
  }