text: /**
   * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license.
   * http://code.google.com/p/rangy/
   *
   * changed in order to be able ...
   *    - to use custom tags
   *    - to detect and replace similar css classes via reg exp
   */
  (function(wysihtml5, rangy) {
  	
  	"use strict";
  	
    var defaultTagName = "span";
    
    var REG_EXP_WHITE_SPACE = /\s+/g;
    
    function hasClass(el, cssClass, regExp) {
      if (!el.className) {
        return false;
      }
      
      var matchingClassNames = el.className.match(regExp) || [];
      return matchingClassNames[matchingClassNames.length - 1] === cssClass;
    }
  
    function addClass(el, cssClass, regExp) {
      if (el.className) {
        removeClass(el, regExp);
        el.className += " " + cssClass;
      } else {
        el.className = cssClass;
      }
    }
  
    function removeClass(el, regExp) {
      if (el.className) {
        el.className = el.className.replace(regExp, "");
      }
    }
    
    function hasSameClasses(el1, el2) {
      return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " ");
    }
  
    function replaceWithOwnChildren(el) {
      var parent = el.parentNode;
      while (el.firstChild) {
        parent.insertBefore(el.firstChild, el);
      }
      parent.removeChild(el);
    }
  
    function elementsHaveSameNonClassAttributes(el1, el2) {
      if (el1.attributes.length != el2.attributes.length) {
        return false;
      }
      for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
        attr1 = el1.attributes[i];
        name = attr1.name;
        if (name != "class") {
          attr2 = el2.attributes.getNamedItem(name);
          if (attr1.specified != attr2.specified) {
            return false;
          }
          if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) {
            return false;
          }
        }
      }
      return true;
    }
  
    function isSplitPoint(node, offset) {
      if (rangy.dom.isCharacterDataNode(node)) {
        if (offset == 0) {
          return !!node.previousSibling;
        } else if (offset == node.length) {
          return !!node.nextSibling;
        } else {
          return true;
        }
      }
  
      return offset > 0 && offset < node.childNodes.length;
    }
  
    function splitNodeAt(node, descendantNode, descendantOffset) {
      var newNode;
      if (rangy.dom.isCharacterDataNode(descendantNode)) {
        if (descendantOffset == 0) {
          descendantOffset = rangy.dom.getNodeIndex(descendantNode);
          descendantNode = descendantNode.parentNode;
        } else if (descendantOffset == descendantNode.length) {
          descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1;
          descendantNode = descendantNode.parentNode;
        } else {
          newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset);
        }
      }
      if (!newNode) {
        newNode = descendantNode.cloneNode(false);
        if (newNode.id) {
          newNode.removeAttribute("id");
        }
        var child;
        while ((child = descendantNode.childNodes[descendantOffset])) {
          newNode.appendChild(child);
        }
        rangy.dom.insertAfter(newNode, descendantNode);
      }
      return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode));
    }
    
    function Merge(firstNode) {
      this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);
      this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
      this.textNodes = [this.firstTextNode];
    }
  
    Merge.prototype = {
      doMerge: function() {
        var textBits = [], textNode, parent, text;
        for (var i = 0, len = this.textNodes.length; i < len; ++i) {
          textNode = this.textNodes[i];
          parent = textNode.parentNode;
          textBits[i] = textNode.data;
          if (i) {
            parent.removeChild(textNode);
            if (!parent.hasChildNodes()) {
              parent.parentNode.removeChild(parent);
            }
          }
        }
        this.firstTextNode.data = text = textBits.join("");
        return text;
      },
  
      getLength: function() {
        var i = this.textNodes.length, len = 0;
        while (i--) {
          len += this.textNodes[i].length;
        }
        return len;
      },
  
      toString: function() {
        var textBits = [];
        for (var i = 0, len = this.textNodes.length; i < len; ++i) {
          textBits[i] = "'" + this.textNodes[i].data + "'";
        }
        return "[Merge(" + textBits.join(",") + ")]";
      }
    };
  
    function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) {
      this.tagNames = tagNames || [defaultTagName];
      this.cssClass = cssClass || "";
      this.similarClassRegExp = similarClassRegExp;
      this.normalize = normalize;
      this.applyToAnyTagName = false;
    }
  
    HTMLApplier.prototype = {
      getAncestorWithClass: function(node) {
        var cssClassMatch;
        while (node) {
          cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true;
          if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) {
            return node;
          }
          node = node.parentNode;
        }
        return false;
      },
  
      // Normalizes nodes after applying a CSS class to a Range.
      postApply: function(textNodes, range) {
        var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
  
        var merges = [], currentMerge;
  
        var rangeStartNode = firstNode, rangeEndNode = lastNode;
        var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
  
        var textNode, precedingTextNode;
  
        for (var i = 0, len = textNodes.length; i < len; ++i) {
          textNode = textNodes[i];
          precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false);
          if (precedingTextNode) {
            if (!currentMerge) {
              currentMerge = new Merge(precedingTextNode);
              merges.push(currentMerge);
            }
            currentMerge.textNodes.push(textNode);
            if (textNode === firstNode) {
              rangeStartNode = currentMerge.firstTextNode;
              rangeStartOffset = rangeStartNode.length;
            }
            if (textNode === lastNode) {
              rangeEndNode = currentMerge.firstTextNode;
              rangeEndOffset = currentMerge.getLength();
            }
          } else {
            currentMerge = null;
          }
        }
  
        // Test whether the first node after the range needs merging
        var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);
        if (nextTextNode) {
          if (!currentMerge) {
            currentMerge = new Merge(lastNode);
            merges.push(currentMerge);
          }
          currentMerge.textNodes.push(nextTextNode);
        }
  
        // Do the merges
        if (merges.length) {
          for (i = 0, len = merges.length; i < len; ++i) {
            merges[i].doMerge();
          }
          // Set the range boundaries
          range.setStart(rangeStartNode, rangeStartOffset);
          range.setEnd(rangeEndNode, rangeEndOffset);
        }
      },
      
      getAdjacentMergeableTextNode: function(node, forward) {
          var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
          var el = isTextNode ? node.parentNode : node;
          var adjacentNode;
          var propName = forward ? "nextSibling" : "previousSibling";
          if (isTextNode) {
            // Can merge if the node's previous/next sibling is a text node
            adjacentNode = node[propName];
            if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) {
              return adjacentNode;
            }
          } else {
            // Compare element with its sibling
            adjacentNode = el[propName];
            if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {
              return adjacentNode[forward ? "firstChild" : "lastChild"];
            }
          }
          return null;
      },
      
      areElementsMergeable: function(el1, el2) {
        return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())
          && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())
          && hasSameClasses(el1, el2)
          && elementsHaveSameNonClassAttributes(el1, el2);
      },
  
      createContainer: function(doc) {
        var el = doc.createElement(this.tagNames[0]);
        if (this.cssClass) {
          el.className = this.cssClass;
        }
        return el;
      },
  
      applyToTextNode: function(textNode) {
        var parent = textNode.parentNode;
        if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
          if (this.cssClass) {
            addClass(parent, this.cssClass, this.similarClassRegExp);
          }
        } else {
          var el = this.createContainer(rangy.dom.getDocument(textNode));
          textNode.parentNode.insertBefore(el, textNode);
          el.appendChild(textNode);
        }
      },
  
      isRemovable: function(el) {
        return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass;
      },
  
      undoToTextNode: function(textNode, range, ancestorWithClass) {
        if (!range.containsNode(ancestorWithClass)) {
          // Split out the portion of the ancestor from which we can remove the CSS class
          var ancestorRange = range.cloneRange();
          ancestorRange.selectNode(ancestorWithClass);
  
          if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) {
            splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset);
            range.setEndAfter(ancestorWithClass);
          }
          if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {
            ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset);
          }
        }
        
        if (this.similarClassRegExp) {
          removeClass(ancestorWithClass, this.similarClassRegExp);
        }
        if (this.isRemovable(ancestorWithClass)) {
          replaceWithOwnChildren(ancestorWithClass);
        }
      },
  
      applyToRange: function(range) {
          var textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
          if (!textNodes.length) {
            try {
              var node = this.createContainer(range.endContainer.ownerDocument);
              range.surroundContents(node);
              this.selectNode(range, node);
              return;
            } catch(e) {}
          }
          
          range.splitBoundaries();
          textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
          
          if (textNodes.length) {
            var textNode;
  
            for (var i = 0, len = textNodes.length; i < len; ++i) {
              textNode = textNodes[i];
              if (!this.getAncestorWithClass(textNode)) {
                this.applyToTextNode(textNode);
              }
            }
            
            range.setStart(textNodes[0], 0);
            textNode = textNodes[textNodes.length - 1];
            range.setEnd(textNode, textNode.length);
            
            if (this.normalize) {
              this.postApply(textNodes, range);
            }
          }
      },
  
      undoToRange: function(range) {
        var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass;
        if (textNodes.length) {
          range.splitBoundaries();
          textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
        } else {
          var doc = range.endContainer.ownerDocument,
              node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
          range.insertNode(node);
          range.selectNode(node);
          textNodes = [node];
        }
        
        for (var i = 0, len = textNodes.length; i < len; ++i) {
          textNode = textNodes[i];
          ancestorWithClass = this.getAncestorWithClass(textNode);
          if (ancestorWithClass) {
            this.undoToTextNode(textNode, range, ancestorWithClass);
          }
        }
        
        if (len == 1) {
          this.selectNode(range, textNodes[0]);
        } else {
          range.setStart(textNodes[0], 0);
          textNode = textNodes[textNodes.length - 1];
          range.setEnd(textNode, textNode.length);
  
          if (this.normalize) {
            this.postApply(textNodes, range);
          }
        }
      },
      
      selectNode: function(range, node) {
        var isElement       = node.nodeType === wysihtml5.ELEMENT_NODE,
            canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : true,
            content         = isElement ? node.innerHTML : node.data,
            isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE);
  
        if (isEmpty && isElement && canHaveHTML) {
          // Make sure that caret is visible in node by inserting a zero width no breaking space
          try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
        }
        range.selectNodeContents(node);
        if (isEmpty && isElement) {
          range.collapse(false);
        } else if (isEmpty) {
          range.setStartAfter(node);
          range.setEndAfter(node);
        }
      },
      
      getTextSelectedByRange: function(textNode, range) {
        var textRange = range.cloneRange();
        textRange.selectNodeContents(textNode);
  
        var intersectionRange = textRange.intersection(range);
        var text = intersectionRange ? intersectionRange.toString() : "";
        textRange.detach();
  
        return text;
      },
  
      isAppliedToRange: function(range) {
        var ancestors = [],
            ancestor,
            textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
        if (!textNodes.length) {
          ancestor = this.getAncestorWithClass(range.startContainer);
          return ancestor ? [ancestor] : false;
        }
        
        for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) {
          selectedText = this.getTextSelectedByRange(textNodes[i], range);
          ancestor = this.getAncestorWithClass(textNodes[i]);
          if (selectedText != "" && !ancestor) {
            return false;
          } else {
            ancestors.push(ancestor);
          }
        }
        return ancestors;
      },
  
      toggleRange: function(range) {
        if (this.isAppliedToRange(range)) {
          this.undoToRange(range);
        } else {
          this.applyToRange(range);
        }
      }
    };
  
    wysihtml5.selection.HTMLApplier = HTMLApplier;
    
  })(wysihtml5, rangy);