6 ], function($, _, Backbone, events) {
 
  11 var TEXT_NODE = Node.TEXT_NODE;
 
  14 var DocumentNode = function(nativeNode, document) {
 
  16         throw new Error('undefined document for a node');
 
  18     this.document = document;
 
  19     this._setNativeNode(nativeNode);
 
  23 $.extend(DocumentNode.prototype, {
 
  24     _setNativeNode: function(nativeNode) {
 
  25         this.nativeNode = nativeNode;
 
  26         this._$ = $(nativeNode);
 
  29     detach: function() { this._$.detach(); },
 
  31     sameNode: function(otherNode) {
 
  32         return otherNode && this.nativeNode === otherNode.nativeNode;
 
  36         return this.nativeNode.parentNode ? this.document.createElementNode(this.nativeNode.parentNode) : null;
 
  39     after: function(node) {
 
  40         node = node instanceof ElementNode ? node : this.document.createElementNode(node);
 
  41         this._$.after(node.nativeNode);
 
  45     before: function(node) {
 
  46         node = node instanceof ElementNode ? node : this.document.createElementNode(node);
 
  47         this._$.before(node.nativeNode);
 
  51     wrapWith: function(node) {
 
  52         node = node instanceof ElementNode ? node : this.document.createElementNode(node);
 
  61     triggerChangeEvent: function(type, metaData) {
 
  62         var event = new events.ChangeEvent(type, $.extend({node: this}, metaData || {}));
 
  63         this.document.trigger('change', event);
 
  67 var ElementNode = function(nativeNode, document) {
 
  68     DocumentNode.call(this, nativeNode, document);
 
  70 ElementNode.prototype = Object.create(DocumentNode.prototype);
 
  72 $.extend(ElementNode.prototype, {
 
  73     nodeType: Node.ELEMENT_NODE,
 
  75     setData: function(key, value) {
 
  76         if(value !== undefined) {
 
  77             this._$.data(key, value);
 
  79             this._$.removeData(_.keys(this._$.data()));
 
  84     getData: function(key) {
 
  86             return this._$.data(key);
 
  88         return this._$.data();
 
  91     getTagName: function() {
 
  92         return this.nativeNode.tagName.toLowerCase();
 
  95     contents: function() {
 
  97             document = this.document;
 
  98         this._$.contents().each(function() {
 
  99             if(this.nodeType === Node.ELEMENT_NODE) {
 
 100                 toret.push(document.createElementNode(this));
 
 102             else if(this.nodeType === Node.TEXT_NODE) {
 
 103                 toret.push(document.createTextNode(this));
 
 109     indexOf: function(node) {
 
 110         return this._$.contents().index(node._$);
 
 113     setTag: function(tagName) {
 
 114         var node = this.document.createElementNode({tagName: tagName}),
 
 115             oldTagName = this.getTagName(),
 
 116             myContents = this._$.contents();
 
 118         this.getAttrs().forEach(function(attribute) {
 
 119             node.setAttr(attribute.name, attribute.value, true);
 
 121         node.setData(this.getData());
 
 123         if(this.sameNode(this.document.root)) {
 
 124             defineDocumentProperties(this.document, node._$);
 
 126         this._$.replaceWith(node._$);
 
 127         this._setNativeNode(node._$[0]);
 
 128         this._$.append(myContents);
 
 129         this.triggerChangeEvent('nodeTagChange', {oldTagName: oldTagName, newTagName: this.getTagName()});
 
 132     getAttr: function(name) {
 
 133         return this._$.attr(name);
 
 136     setAttr: function(name, value, silent) {
 
 137         var oldVal = this.getAttr(name);
 
 138         this._$.attr(name, value);
 
 140             this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
 
 144     getAttrs: function() {
 
 146         for(var i = 0; i < this.nativeNode.attributes.length; i++) {
 
 147             toret.push(this.nativeNode.attributes[i]);
 
 152     append: function(node) {
 
 153         node = node instanceof DocumentNode ? node : this.document.createElementNode(node);
 
 154         this._$.append(node.nativeNode);
 
 157     prepend: function(node) {
 
 158         node = node instanceof DocumentNode ? node : this.document.createElementNode(node);
 
 159         this._$.prepend(node.nativeNode);
 
 162     unwrapContent: function() {
 
 163         var parent = this.parent();
 
 168         var parentContents = parent.contents(),
 
 169             myContents = this.contents(),
 
 170             myIdx = parent.indexOf(this);
 
 172         if(myContents.length === 0) {
 
 173             return this.detach();
 
 176         var moveLeftRange, moveRightRange, leftMerged;
 
 178         if(myIdx > 0 && (parentContents[myIdx-1].nodeType === TEXT_NODE) && (myContents[0].nodeType === TEXT_NODE)) {
 
 179             parentContents[myIdx-1].appendText(myContents[0].getText());
 
 180             myContents[0].detach();
 
 181             moveLeftRange = true;
 
 187         if(!(leftMerged && myContents.length === 1)) {
 
 188             if(myIdx < parentContents.length - 1 && (parentContents[myIdx+1].nodeType === TEXT_NODE) && (myContents[myContents.length-1].nodeType === TEXT_NODE)) {
 
 189                 parentContents[myIdx+1].prependText(myContents[myContents.length-1].getText());
 
 190                 myContents[myContents.length-1].detach();
 
 191                 moveRightRange = true;
 
 195         var childrenLength = this.contents().length;
 
 196         this.contents().forEach(function(child) {
 
 203             element1: parent.contents()[myIdx + (moveLeftRange ? -1 : 0)],
 
 204             element2: parent.contents()[myIdx + childrenLength-1 + (moveRightRange ? 1 : 0)]
 
 208     wrapText: function(params) {
 
 209         return this.document._wrapText(_.extend({inside: this}, params));
 
 213         var wrapper = $('<div>');
 
 214         wrapper.append(this._getXMLDOMToDump());
 
 215         return wrapper.html();
 
 218     _getXMLDOMToDump: function() {
 
 223 var TextNode = function(nativeNode, document) {
 
 224     DocumentNode.call(this, nativeNode, document);
 
 226 TextNode.prototype = Object.create(DocumentNode.prototype);
 
 228 $.extend(TextNode.prototype, {
 
 229     nodeType: Node.TEXT_NODE,
 
 231     getText: function() {
 
 232         return this.nativeNode.data;
 
 235     setText: function(text) {
 
 236         this.nativeNode.data = text;
 
 237         this.triggerTextChangeEvent();
 
 240     appendText: function(text) {
 
 241         this.nativeNode.data = this.nativeNode.data + text;
 
 242         this.triggerTextChangeEvent();
 
 245     prependText: function(text) {
 
 246         this.nativeNode.data = text + this.nativeNode.data;
 
 247         this.triggerTextChangeEvent();
 
 250     wrapWith: function(desc) {
 
 251         if(typeof desc.start === 'number' && typeof desc.end === 'number') {
 
 252             return this.document._wrapText({
 
 253                 inside: this.parent(),
 
 254                 textNodeIdx: this.parent().indexOf(this),
 
 255                 offsetStart: Math.min(desc.start, desc.end),
 
 256                 offsetEnd: Math.max(desc.start, desc.end),
 
 257                 _with: {tag: desc.tagName, attrs: desc.attrs}
 
 260             return DocumentNode.prototype.wrapWith.call(this, desc);
 
 264     triggerTextChangeEvent: function() {
 
 265         var event = new events.ChangeEvent('nodeTextChange', {node: this});
 
 266         this.document.trigger('change', event);
 
 271 var parseXML = function(xml) {
 
 275 var Document = function(xml) {
 
 279 $.extend(Document.prototype, Backbone.Events, {
 
 280     ElementNodeFactory: ElementNode,
 
 281     TextNodeFactory: TextNode,
 
 283     createElementNode: function(from) {
 
 284         if(!(from instanceof HTMLElement)) {
 
 286                 from = document.createTextNode(from.text);
 
 288                 var node = $('<' + from.tagName + '>');
 
 290                 _.keys(from.attrs || {}).forEach(function(key) {
 
 291                     node.attr(key, from.attrs[key]);
 
 297         return new this.ElementNodeFactory(from, this);
 
 300     createTextNode: function(nativeNode) {
 
 301         return new this.TextNodeFactory(nativeNode, this);
 
 304     loadXML: function(xml, options) {
 
 305         options = options || {};
 
 306         defineDocumentProperties(this, $(parseXML(xml)));
 
 307         if(!options.silent) {
 
 308             this.trigger('contentSet');
 
 313         return this.root.toXML();
 
 316     _wrapText: function(params) {
 
 317         params = _.extend({textNodeIdx: 0}, params);
 
 318         if(typeof params.textNodeIdx === 'number') {
 
 319             params.textNodeIdx = [params.textNodeIdx];
 
 322         var contentsInside = params.inside.contents(),
 
 323             idx1 = Math.min.apply(Math, params.textNodeIdx),
 
 324             idx2 = Math.max.apply(Math, params.textNodeIdx),
 
 325             textNode1 = contentsInside[idx1],
 
 326             textNode2 = contentsInside[idx2],
 
 327             sameNode = textNode1.sameNode(textNode2),
 
 328             prefixOutside = textNode1.getText().substr(0, params.offsetStart),
 
 329             prefixInside = textNode1.getText().substr(params.offsetStart),
 
 330             suffixInside = textNode2.getText().substr(0, params.offsetEnd),
 
 331             suffixOutside = textNode2.getText().substr(params.offsetEnd)
 
 334         if(!(textNode1.parent().sameNode(textNode2.parent()))) {
 
 335             throw new Error('Wrapping text in non-sibling text nodes not supported.');
 
 338         var wrapperElement = this.createElementNode({tagName: params._with.tag, attrs: params._with.attrs});
 
 339         textNode1.after(wrapperElement);
 
 342         if(prefixOutside.length > 0) {
 
 343             wrapperElement.before({text:prefixOutside});
 
 346             var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
 
 347             wrapperElement.append({text: core});
 
 350             if(prefixInside.length > 0) {
 
 351                 wrapperElement.append({text: prefixInside});
 
 353             for(var i = idx1 + 1; i < idx2; i++) {
 
 354                 wrapperElement.append(contentsInside[i]);
 
 356             if(suffixInside.length > 0) {
 
 357                 wrapperElement.append({text: suffixInside});
 
 360         if(suffixOutside.length > 0) {
 
 361             wrapperElement.after({text: suffixOutside});
 
 363         return wrapperElement;
 
 367 var defineDocumentProperties = function(doc, $document) {
 
 368     Object.defineProperty(doc, 'root', {get: function() {
 
 369         return doc.createElementNode($document[0]);
 
 370     }, configurable: true});
 
 371     Object.defineProperty(doc, 'dom', {get: function() {
 
 373     }, configurable: true});
 
 377     documentFromXML: function(xml) {
 
 378         return new Document(parseXML(xml));
 
 381     elementNodeFromXML: function(xml) {
 
 382         return this.documentFromXML(xml).root;
 
 386     DocumentNode: DocumentNode,
 
 387     ElementNode: ElementNode