6     'smartxml/transformations',
 
   9 ], function($, _, Backbone, events, transformations, coreTransformations, fragments) {
 
  15 var privateKey = '_smartxml';
 
  17 var DocumentNode = function(nativeNode, document) {
 
  19         throw new Error('undefined document for a node');
 
  21     this.document = document;
 
  23     this._setNativeNode(nativeNode);
 
  27 $.extend(DocumentNode.prototype, {
 
  29     getProperty: function(propName) {
 
  30         var toret = this.object[propName];
 
  31         if(toret && _.isFunction(toret)) {
 
  32             toret = toret.call(this);
 
  37     transform: function(Transformation, args) {
 
  38         var transformation = new Transformation(this.document, this, args);
 
  39         return this.document.transform(transformation);
 
  42     _setNativeNode: function(nativeNode) {
 
  43         this.nativeNode = nativeNode;
 
  44         this._$ = $(nativeNode);
 
  48         var clone = this._$.clone(true, true),
 
  50         clone.find('*').addBack().each(function() {
 
  52                 clonedData = $(this).data();
 
  53             $(el).removeData(privateKey);
 
  54             _.pairs(clonedData).forEach(function(pair) {
 
  57                 if(_.isFunction(value.clone)) {
 
  58                     clonedData[key] = value.clone(node.document.createDocumentNode(el));
 
  62         return this.document.createDocumentNode(clone[0]);
 
  65     getPath: function(ancestor) {
 
  66         if(!(this.document.containsNode(this))) {
 
  70         var nodePath = [this].concat(this.parents()),
 
  72         ancestor = ancestor || this.document.root;
 
  74         nodePath.some(function(node, i) {
 
  75             if(node.sameNode(ancestor)) {
 
  81         if(idx !== undefined) {
 
  82             nodePath = nodePath.slice(0, idx);
 
  84         toret = nodePath.map(function(node) {return node.getIndex(); });
 
  90         return this.document.root.sameNode(this);
 
  93     isInDocument: function() {
 
  94         return this.document.containsNode(this);
 
  97     isSiblingOf: function(node) {
 
  98         return node && this.parent().sameNode(node.parent());
 
 101     sameNode: function(otherNode) {
 
 102         return !!(otherNode) && this.nativeNode === otherNode.nativeNode;
 
 106         var parentNode = this.nativeNode.parentNode;
 
 107         if(parentNode && parentNode.nodeType === Node.ELEMENT_NODE) {
 
 108             return this.document.createDocumentNode(parentNode);
 
 113     parents: function() {
 
 114         var parent = this.parent(),
 
 115             parents = parent ? parent.parents() : [];
 
 117             parents.unshift(parent);
 
 123         var myIdx = this.getIndex();
 
 124         return myIdx > 0 ? this.parent().contents()[myIdx-1] : null;
 
 131         var myIdx = this.getIndex(),
 
 132             parentContents = this.parent().contents();
 
 133         return myIdx < parentContents.length - 1 ? parentContents[myIdx+1] : null;
 
 136     isSurroundedByTextNodes: function() {
 
 137         return this.isPrecededByTextNode() && this.isFollowedByTextNode();
 
 140     isPrecededByTextNode: function() {
 
 141         var prev = this.prev();
 
 142         return prev && prev.nodeType === Node.TEXT_NODE;
 
 145     isFollowedByTextNode: function() {
 
 146         var next = this.next();
 
 147         return next && next.nodeType === Node.TEXT_NODE;
 
 150     triggerChangeEvent: function(type, metaData, origParent, nodeWasContained) {
 
 151         var node = (metaData && metaData.node) ? metaData.node : this,
 
 152             event = new events.ChangeEvent(type, $.extend({node: node}, metaData || {}));
 
 153         if(type === 'nodeDetached' || this.document.containsNode(event.meta.node)) {
 
 154             this.document.trigger('change', event);
 
 156         if(type === 'nodeAdded' && !this.document.containsNode(this) && nodeWasContained) {
 
 157              event = new events.ChangeEvent('nodeDetached', {node: node, parent: origParent});
 
 158              this.document.trigger('change', event);
 
 162     getNodeInsertion: function(node) {
 
 163         return this.document.getNodeInsertion(node);
 
 166     getIndex: function() {
 
 173         parent = this.parent();
 
 174         return parent ? parent.indexOf(this) : undefined;
 
 177     getNearestElementNode: function() {
 
 178         return this.nodeType === Node.ELEMENT_NODE ? this : this.parent();
 
 183 var ElementNode = function(nativeNode, document) {
 
 184     DocumentNode.call(this, nativeNode, document);
 
 185     $(nativeNode).data(privateKey, {node: this});
 
 187 ElementNode.prototype = Object.create(DocumentNode.prototype);
 
 189 $.extend(ElementNode.prototype, {
 
 190     nodeType: Node.ELEMENT_NODE,
 
 192     setData: function(arg1, arg2) {
 
 193         if(arguments.length === 2) {
 
 194             if(_.isUndefined(arg2)) {
 
 195                 this._$.removeData(arg1);
 
 197                 this._$.data(arg1, arg2);
 
 200             this._$.removeData(_.keys(this._$.data()));
 
 205     getData: function(key) {
 
 207             return this._$.data(key);
 
 209         var toret = _.clone(this._$.data());
 
 210         delete toret[privateKey];
 
 214     getTagName: function() {
 
 215         return this.nativeNode.tagName.toLowerCase();
 
 218     contents: function(selector) {
 
 220             document = this.document;
 
 222             this._$.children(selector).each(function() {
 
 223                 toret.push(document.createDocumentNode(this));
 
 226             this._$.contents().each(function() {
 
 227                 toret.push(document.createDocumentNode(this));
 
 233     indexOf: function(node) {
 
 234         return this._$.contents().index(node._$);
 
 237     getAttr: function(name) {
 
 238         return this._$.attr(name);
 
 241     getAttrs: function() {
 
 243         for(var i = 0; i < this.nativeNode.attributes.length; i++) {
 
 244             toret.push(this.nativeNode.attributes[i]);
 
 249     containsNode: function(node) {
 
 250         return node && (node.nativeNode === this.nativeNode || node._$.parents().index(this._$) !== -1);
 
 253     getLastTextNode: function() {
 
 254         var contents = this.contents(),
 
 257         contents.reverse().some(function(node) {
 
 258             if(node.nodeType === Node.TEXT_NODE) {
 
 262             toret = node.getLastTextNode();
 
 270         var wrapper = $('<div>');
 
 271         wrapper.append(this._getXMLDOMToDump());
 
 272         return wrapper.html();
 
 275     _getXMLDOMToDump: function() {
 
 281 var TextNode = function(nativeNode, document) {
 
 282     DocumentNode.call(this, nativeNode, document);
 
 283     this._data = Object.create({});
 
 284     nativeNode.__smartxmlTextNodeInstance = this;
 
 286 TextNode.prototype = Object.create(DocumentNode.prototype);
 
 288 $.extend(TextNode.prototype, {
 
 289     nodeType: Node.TEXT_NODE,
 
 291     setData: function(arg1, arg2) {
 
 292         if(arguments.length === 2) {
 
 293             if(_.isUndefined(arg2)) {
 
 294                 delete this._data[arg1];
 
 296                 this._data[arg1] = arg2;
 
 299             this._data = _.extend({}, arg1);
 
 303     getData: function(key) {
 
 305             return this._data[key];
 
 310     getText: function() {
 
 311         return this.nativeNode.data;
 
 315     containsNode: function() {
 
 319     triggerTextChangeEvent: function() {
 
 320         var event = new events.ChangeEvent('nodeTextChange', {node: this});
 
 321         this.document.trigger('change', event);
 
 326 var parseXML = function(xml) {
 
 327     var toret = $($.trim(xml));
 
 328     if(toret.length !== 1) {
 
 329         throw new Error('Unable to parse XML: ' + xml);
 
 335 var registerTransformation = function(desc, name, target) {
 
 336     var Transformation = transformations.createContextTransformation(desc, name);
 
 337     target[name] = function() {
 
 339             args = Array.prototype.slice.call(arguments, 0);
 
 340         return instance.transform(Transformation, args);
 
 344 var registerMethod = function(methodName, method, target) {
 
 345     if(target[methodName]) {
 
 346         throw new Error('Cannot extend {target} with method name {methodName}. Name already exists.'
 
 347             .replace('{target}', target)
 
 348             .replace('{methodName}', methodName)
 
 351     target[methodName] = method;
 
 355 var Document = function(xml, extensions) {
 
 358     this._currentTransaction = null;
 
 359     this._transformationLevel = 0;
 
 361     this._nodeMethods = {};
 
 362     this._textNodeMethods = {};
 
 363     this._elementNodeMethods = {};
 
 364     this._nodeTransformations = {};
 
 365     this._textNodeTransformations = {};
 
 366     this._elementNodeTransformations = {};
 
 368     this.registerExtension(coreTransformations);
 
 370     (extensions || []).forEach(function(extension) {
 
 371         this.registerExtension(extension);
 
 376 $.extend(Document.prototype, Backbone.Events, fragments, {
 
 377     ElementNodeFactory: ElementNode,
 
 378     TextNodeFactory: TextNode,
 
 380     createDocumentNode: function(from) {
 
 383         if(from instanceof Node) {
 
 385             cached = from instanceof Text ? from.__smartxmlTextNodeInstance : ($(from).data(privateKey) || {}).node;
 
 386             if(cached instanceof DocumentNode) {
 
 390             if(typeof from === 'string') {
 
 391                 from = parseXML(from);
 
 392                 this.normalizeXML(from);
 
 394                 if(from.text !== undefined) {
 
 395                     /* globals document */
 
 396                     from = document.createTextNode(from.text);
 
 399                         throw new Error('tagName missing');
 
 401                     var node = $('<' + from.tagName + '>');
 
 403                     _.keys(from.attrs || {}).forEach(function(key) {
 
 404                         node.attr(key, from.attrs[key]);
 
 411         var Factory, typeMethods, typeTransformations;
 
 412         if(from.nodeType === Node.TEXT_NODE) {
 
 413             Factory = this.TextNodeFactory;
 
 414             typeMethods = this._textNodeMethods;
 
 415             typeTransformations = this._textNodeTransformations;
 
 416         } else if(from.nodeType === Node.ELEMENT_NODE) {
 
 417             Factory = this.ElementNodeFactory;
 
 418             typeMethods = this._elementNodeMethods;
 
 419             typeTransformations = this._elementNodeTransformations;
 
 421         var toret = new Factory(from, this);
 
 422         _.extend(toret, this._nodeMethods);
 
 423         _.extend(toret, typeMethods);
 
 425         _.extend(toret, this._nodeTransformations);
 
 426         _.extend(toret, typeTransformations);
 
 428         toret.__super__ = _.extend({}, this._nodeMethods, this._nodeTransformations);
 
 429         _.keys(toret.__super__).forEach(function(key) {
 
 430             toret.__super__[key] = _.bind(toret.__super__[key], toret);
 
 436     loadXML: function(xml, options) {
 
 437         options = options || {};
 
 438         this._defineDocumentProperties($(parseXML(xml)));
 
 439         this.normalizeXML(this.dom);
 
 440         if(!options.silent) {
 
 441             this.trigger('contentSet');
 
 445     normalizeXML: function(nativeNode) {
 
 446         void(nativeNode); // noop
 
 450         return this.root.toXML();
 
 453     containsNode: function(node) {
 
 454         return this.root && this.root.containsNode(node);
 
 457     getSiblingParents: function(params) {
 
 458         var parents1 = [params.node1].concat(params.node1.parents()).reverse(),
 
 459             parents2 = [params.node2].concat(params.node2.parents()).reverse(),
 
 460             noSiblingParents = null;
 
 462         if(parents1.length === 0 || parents2.length === 0 || !(parents1[0].sameNode(parents2[0]))) {
 
 463             return noSiblingParents;
 
 466         var stop = Math.min(parents1.length, parents2.length),
 
 468         for(i = 0; i < stop; i++) {
 
 469             if(parents1[i].sameNode(parents2[i])) {
 
 477         return {node1: parents1[i], node2: parents2[i]};
 
 480     trigger: function() {
 
 481         Backbone.Events.trigger.apply(this, arguments);
 
 484     getNodeInsertion: function(node) {
 
 486         if(node instanceof DocumentNode) {
 
 487             insertion.ofNode = node;
 
 488             insertion.insertsNew = !this.containsNode(node);
 
 490           insertion.ofNode = this.createDocumentNode(node);
 
 491           insertion.insertsNew = true;
 
 496     registerMethod: function(methodName, method, dstName) {
 
 500             documentNode: doc._nodeMethods,
 
 501             textNode: doc._textNodeMethods,
 
 502             elementNode: doc._elementNodeMethods
 
 504         registerMethod(methodName, method, destination);
 
 507     registerTransformation: function(desc, name, dstName) {
 
 511             documentNode: doc._nodeTransformations,
 
 512             textNode: doc._textNodeTransformations,
 
 513             elementNode: doc._elementNodeTransformations
 
 515         registerTransformation(desc, name, destination);
 
 518     registerExtension: function(extension) {
 
 521         ['document', 'documentNode', 'elementNode', 'textNode'].forEach(function(dstName) {
 
 522             var dstExtension = extension[dstName];
 
 524                 if(dstExtension.methods) {
 
 525                     _.pairs(dstExtension.methods).forEach(function(pair) {
 
 526                         var methodName = pair[0],
 
 529                         doc.registerMethod(methodName, method, dstName);
 
 534                 if(dstExtension.transformations) {
 
 535                     _.pairs(dstExtension.transformations).forEach(function(pair) {
 
 538                         doc.registerTransformation(desc, name, dstName);
 
 545     ifChanged: function(context, action, documentChangedHandler, documentUnchangedHandler) {
 
 546         var hasChanged = false,
 
 547             changeMonitor = function() {
 
 551         this.on('change', changeMonitor);
 
 552         action.call(context);
 
 553         this.off('change', changeMonitor);
 
 556             if(documentChangedHandler) {
 
 557                 documentChangedHandler.call(context);
 
 560             if(documentUnchangedHandler) {
 
 561                 documentUnchangedHandler.call(context);
 
 566     transform: function(Transformation, args) {
 
 567         var toret, transformation;
 
 569         if(!this._currentTransaction) {
 
 570             return this.transaction(function() {
 
 571                 return this.transform(Transformation, args);
 
 575         if(typeof Transformation === 'function') {
 
 576             transformation = new Transformation(this, this, args);
 
 578             transformation = Transformation;
 
 581             this._transformationLevel++;
 
 586                     toret = transformation.run({beUndoable:this._transformationLevel === 1});
 
 589                     if(this._transformationLevel === 1 && !this._undoInProgress) {
 
 590                         this._currentTransaction.pushTransformation(transformation);
 
 596             this._transformationLevel--;
 
 599             throw new Error('Transformation ' + transformation + ' doesn\'t exist!');
 
 603         var transaction = this.undoStack.pop(),
 
 605             transformations, stopAt;
 
 608             this._undoInProgress = true;
 
 610             // We will modify this array in a minute so make sure we work on a copy.
 
 611             transformations = transaction.transformations.slice(0);
 
 613             if(transformations.length > 1) {
 
 614                 // In case of real transactions we don't want to run undo on all of transformations if we don't have to.
 
 615                 transformations.some(function(t, idx) {
 
 616                     if(!t.undo && t.getChangeRoot().sameNode(doc.root)) {
 
 621                 if(stopAt !== undefined) {
 
 622                     // We will get away with undoing only this transformations as the one at stopAt reverses the whole document.
 
 623                     transformations = transformations.slice(0, stopAt+1);
 
 627             transformations.reverse();
 
 628             transformations.forEach(function(t) {
 
 632             this._undoInProgress = false;
 
 633             this.redoStack.push(transaction);
 
 634             this.trigger('operationEnd');
 
 638         var transaction = this.redoStack.pop();
 
 640             this._transformationLevel++;
 
 641             transaction.transformations.forEach(function(t) {
 
 642                 t.run({beUndoable: true});
 
 644             this._transformationLevel--;
 
 645             this.undoStack.push(transaction);
 
 646             this.trigger('operationEnd');
 
 651     startTransaction: function(metadata) {
 
 652         if(this._currentTransaction) {
 
 653             throw new Error('Nested transactions not supported!');
 
 655         this._rollbackBackup = this.root.clone();
 
 656         this._currentTransaction = new Transaction([], metadata);
 
 659     endTransaction: function() {
 
 660         if(!this._currentTransaction) {
 
 661             throw new Error('End of transaction requested, but there is no transaction in progress!');
 
 663         if(this._currentTransaction.hasTransformations()) {
 
 664             this.undoStack.push(this._currentTransaction);
 
 665             this.trigger('operationEnd');
 
 667         this._currentTransaction = null;
 
 670     rollbackTransaction: function() {
 
 671         if(!this._currentTransaction) {
 
 672             throw new Error('Transaction rollback requested, but there is no transaction in progress!');
 
 674         this.replaceRoot(this._rollbackBackup);
 
 675         this._rollbackBackup = null;
 
 676         this._currentTransaction = null;
 
 677         this._transformationLevel = 0;
 
 680     transaction: function(callback, params) {
 
 682         params = params || {};
 
 683         this.startTransaction(params.metadata);
 
 685             toret = callback.call(params.context || this);
 
 690             this.rollbackTransaction();
 
 693         this.endTransaction();
 
 695             params.success(toret);
 
 700     getNodeByPath: function(path) {
 
 701         var toret = this.root;
 
 702         path.some(function(idx) {
 
 703             toret = toret.contents()[idx];
 
 711     _defineDocumentProperties: function($document) {
 
 713         Object.defineProperty(doc, 'root', {get: function() {
 
 717             return doc.createDocumentNode($document[0]);
 
 718         }, configurable: true});
 
 719         Object.defineProperty(doc, 'dom', {get: function() {
 
 724         }, configurable: true});
 
 727     createFragment: function(Type, params) {
 
 728         if(!Type.prototype instanceof fragments.Fragment) {
 
 729             throw new Error('Can\'t create a fragment: `Type` is not a valid Fragment');
 
 731         return new Type(this, params);
 
 735 var Transaction = function(transformations, metadata) {
 
 736     this.transformations = transformations || [];
 
 737     this.metadata = metadata;
 
 739 $.extend(Transaction.prototype, {
 
 740     pushTransformation: function(transformation) {
 
 741         this.transformations.push(transformation);
 
 743     hasTransformations: function() {
 
 744         return this.transformations.length > 0;
 
 750     documentFromXML: function(xml) {
 
 751         var doc = new Document(xml);
 
 755     elementNodeFromXML: function(xml) {
 
 756         return this.documentFromXML(xml).root;
 
 760     DocumentNode: DocumentNode,
 
 761     ElementNode: ElementNode,