+var TextNode = function(nativeNode, document) {
+ DocumentNode.call(this, nativeNode, document);
+};
+TextNode.prototype = Object.create(DocumentNode.prototype);
+
+$.extend(TextNode.prototype, {
+ nodeType: Node.TEXT_NODE,
+
+ getText: function() {
+ return this.nativeNode.data;
+ },
+
+
+ containsNode: function() {
+ return false;
+ },
+
+ triggerTextChangeEvent: function() {
+ var event = new events.ChangeEvent('nodeTextChange', {node: this});
+ this.document.trigger('change', event);
+ }
+});
+
+
+var parseXML = function(xml) {
+ var toret = $($.trim(xml));
+ if(!toret.length) {
+ throw new Error('Unable to parse XML: ' + xml);
+ }
+ return toret[0];
+
+};
+
+var registerTransformation = function(desc, name, target) {
+ var Transformation = transformations.createContextTransformation(desc, name);
+ target[name] = function() {
+ var instance = this,
+ args = Array.prototype.slice.call(arguments, 0);
+ return instance.transform(Transformation, args);
+ };
+};
+
+var registerMethod = function(methodName, method, target) {
+ if(target[methodName]) {
+ throw new Error('Cannot extend {target} with method name {methodName}. Name already exists.'
+ .replace('{target}', target)
+ .replace('{methodName}', methodName)
+ );
+ }
+ target[methodName] = method;
+};
+
+
+var Document = function(xml, extensions) {
+ this.undoStack = [];
+ this.redoStack = [];
+ this._currentTransaction = null;
+ this._transformationLevel = 0;
+
+ this._nodeMethods = {};
+ this._textNodeMethods = {};
+ this._elementNodeMethods = {};
+ this._nodeTransformations = {};
+ this._textNodeTransformations = {};
+ this._elementNodeTransformations = {};
+
+ this.registerExtension(coreTransformations);
+
+ (extensions || []).forEach(function(extension) {
+ this.registerExtension(extension);
+ }.bind(this));
+ this.loadXML(xml);
+};
+
+$.extend(Document.prototype, Backbone.Events, fragments, {
+ ElementNodeFactory: ElementNode,
+ TextNodeFactory: TextNode,
+
+ createDocumentNode: function(from) {
+ if(!(from instanceof Node)) {
+ if(typeof from === 'string') {
+ from = parseXML(from);
+ this.normalizeXML(from);
+ } else {
+ if(from.text !== undefined) {
+ /* globals document */
+ from = document.createTextNode(from.text);
+ } else {
+ if(!from.tagName) {
+ throw new Error('tagName missing');
+ }
+ var node = $('<' + from.tagName + '>');
+
+ _.keys(from.attrs || {}).forEach(function(key) {
+ node.attr(key, from.attrs[key]);
+ });
+
+ from = node[0];
+ }
+ }
+ }
+ var Factory, typeMethods, typeTransformations;
+ if(from.nodeType === Node.TEXT_NODE) {
+ Factory = this.TextNodeFactory;
+ typeMethods = this._textNodeMethods;
+ typeTransformations = this._textNodeTransformations;
+ } else if(from.nodeType === Node.ELEMENT_NODE) {
+ Factory = this.ElementNodeFactory;
+ typeMethods = this._elementNodeMethods;
+ typeTransformations = this._elementNodeTransformations;
+ }
+ var toret = new Factory(from, this);
+ _.extend(toret, this._nodeMethods);
+ _.extend(toret, typeMethods);
+
+ _.extend(toret, this._nodeTransformations);
+ _.extend(toret, typeTransformations);
+
+ toret.__super__ = _.extend({}, this._nodeMethods, this._nodeTransformations);
+ _.keys(toret.__super__).forEach(function(key) {
+ toret.__super__[key] = _.bind(toret.__super__[key], toret);
+ });
+
+ return toret;
+ },
+
+ loadXML: function(xml, options) {
+ options = options || {};
+ this._defineDocumentProperties($(parseXML(xml)));
+ this.normalizeXML(this.dom);
+ if(!options.silent) {
+ this.trigger('contentSet');
+ }
+ },
+
+ normalizeXML: function(nativeNode) {
+ void(nativeNode); // noop
+ },
+
+ toXML: function() {
+ return this.root.toXML();
+ },
+
+ containsNode: function(node) {
+ return this.root && this.root.containsNode(node);
+ },
+
+ getSiblingParents: function(params) {
+ var parents1 = [params.node1].concat(params.node1.parents()).reverse(),
+ parents2 = [params.node2].concat(params.node2.parents()).reverse(),
+ noSiblingParents = null;
+
+ if(parents1.length === 0 || parents2.length === 0 || !(parents1[0].sameNode(parents2[0]))) {
+ return noSiblingParents;
+ }
+
+ var stop = Math.min(parents1.length, parents2.length),
+ i;
+ for(i = 0; i < stop; i++) {
+ if(parents1[i].sameNode(parents2[i])) {
+ continue;
+ }
+ break;
+ }
+ if(i === stop) {
+ i--;
+ }
+ return {node1: parents1[i], node2: parents2[i]};
+ },
+
+ trigger: function() {
+ Backbone.Events.trigger.apply(this, arguments);
+ },
+
+ getNodeInsertion: function(node) {
+ var insertion = {};
+ if(node instanceof DocumentNode) {
+ insertion.ofNode = node;
+ insertion.insertsNew = !this.containsNode(node);