smartxml: implementing merging of inserted text nodes if necessary
authorAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Thu, 23 Jan 2014 08:37:47 +0000 (09:37 +0100)
committerAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Thu, 23 Jan 2014 08:37:47 +0000 (09:37 +0100)
This ensures that document stays in a normalized state, i.e.
without adjacent text nodes. Up until now it was the client
code responsibility not to denormalize the document.

src/smartxml/core.js
src/smartxml/smartxml.test.js

index 4d3c2c1..ae6d20e 100644 (file)
@@ -8,16 +8,20 @@ var _ = require('libs/underscore'),
 
 
 var INSERTION = function(implementation) {
-    var toret = function(node) {
+    var toret = function(node, options) {
         var insertion = this.getNodeInsertion(node),
             nodeWasContained = this.document.containsNode(insertion.ofNode),
-            nodeParent;
+            nodeParent,
+            returned;
+        options = options || {};
         if(!(this.document.containsNode(this))) {
             nodeParent = insertion.ofNode.parent();
         }
-        implementation.call(this, insertion.ofNode.nativeNode);
-        this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode}, nodeParent, nodeWasContained);
-        return insertion.ofNode;
+        returned = implementation.call(this, insertion.ofNode);
+        if(!options.silent && returned.sameNode(insertion.ofNode)) {
+            this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode}, nodeParent, nodeWasContained);
+        }
+        return returned;
     };
     return toret;
 };
@@ -40,12 +44,24 @@ var documentNodeTransformations = {
         return toret;
     },
 
-    after: INSERTION(function(nativeNode) {
-        return this._$.after(nativeNode);
+    after: INSERTION(function(node) {
+        var next = this.next();
+        if(next && next.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
+            next.setText(node.getText() + next.getText());
+            return next;
+        }
+        this._$.after(node.nativeNode);
+        return node;
     }),
 
-    before: INSERTION(function(nativeNode) {
-        return this._$.before(nativeNode);
+    before: INSERTION(function(node) {
+        var prev = this.prev();
+        if(prev && prev.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
+            prev.setText(prev.getText() + node.getText());
+            return prev;
+        }
+        this._$.before(node.nativeNode);
+        return node;
     }),
 
     wrapWith: function(node) {
@@ -116,12 +132,26 @@ var elementNodeTransformations = {
         }
     },
 
-    append: INSERTION(function(nativeNode) {
-        this._$.append(nativeNode);
+    append: INSERTION(function(node) {
+        var last = _.last(this.contents());
+        if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
+            last.setText(last.getText() + node.getText());
+            return last;
+        } else {
+            this._$.append(node.nativeNode);
+            return node;
+        }
     }),
 
-    prepend: INSERTION(function(nativeNode) {
-        this._$.prepend(nativeNode);
+    prepend: INSERTION(function(node) {
+        var first = this.contents()[0];
+        if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
+            first.setText(node.getText() + first.getText());
+            return first;
+        } else {
+            this._$.prepend(node.nativeNode);
+            return node;
+        }
     }),
 
     insertAtIndex: function(nativeNode, index) {
@@ -199,6 +229,24 @@ var textNodeTransformations = {
         }
     },
 
+    before: INSERTION(function(node) {
+        if(node.nodeType === Node.TEXT_NODE) {
+            this.prependText(node.getText());
+            return this;
+        } else {
+            return this.__super__.before(node, {silent:true});
+        }
+    }),
+
+    after: INSERTION(function(node) {
+        if(node.nodeType === Node.TEXT_NODE) {
+            this.appendText(node.getText());
+            return this;
+        } else {
+            return this.__super__.after(node, {silent:true});
+        }
+    }),
+
     appendText: function(text) {
         this.nativeNode.data = this.nativeNode.data + text;
         this.triggerTextChangeEvent();
index e6f0baa..542237a 100644 (file)
@@ -458,6 +458,68 @@ describe('smartxml', function() {
             expect(node1.contents()[1].sameNode(node2)).to.be.true;
         });
 
+        describe('adding text nodes', function() {
+            it('merges text nodes on append', function() {
+                var doc = getDocumentFromXML('<root>text1</root>'),
+                    returned;
+                returned = doc.root.append({text: 'text2'});
+                expect(doc.root.contents().length).to.equal(1);
+                expect(returned.sameNode(doc.root.contents()[0])).to.equal(true, 'modified node returned');
+                expect(doc.root.contents()[0].getText()).to.equal('text1text2');
+            });
+
+            it('merges text nodes on prepend', function() {
+                var doc = getDocumentFromXML('<root>text1</root>'),
+                    returned;
+                returned = doc.root.prepend({text: 'text2'});
+                expect(doc.root.contents().length).to.equal(1);
+                expect(returned.sameNode(doc.root.contents()[0])).to.equal(true, 'modified node returned');
+                expect(doc.root.contents()[0].getText()).to.equal('text2text1');
+            });
+
+            it('merges text nodes on before text node', function() {
+                var doc = getDocumentFromXML('<root>text1</root>'),
+                    textNode = doc.root.contents()[0],
+                    returned;
+                returned = textNode.before({text: 'text2'});
+                expect(doc.root.contents().length).to.equal(1);
+                expect(returned.sameNode(doc.root.contents()[0])).to.equal(true, 'modified node returned');
+                expect(doc.root.contents()[0].getText()).to.equal('text2text1');
+            });
+
+            it('merges text nodes on after text node', function() {
+                var doc = getDocumentFromXML('<root>text1</root>'),
+                    textNode = doc.root.contents()[0],
+                    returned;
+                returned = textNode.after({text: 'text2'});
+                expect(doc.root.contents().length).to.equal(1);
+                expect(returned.sameNode(doc.root.contents()[0])).to.equal(true, 'modified node returned');
+                expect(doc.root.contents()[0].getText()).to.equal('text1text2');
+            });
+
+            it('merges text nodes on before element node', function() {
+                var doc = getDocumentFromXML('<root>text1<div></div></root>'),
+                    textNode = doc.root.contents()[0],
+                    div = doc.root.contents()[1],
+                    returned;
+                returned = div.before({text: 'text2'});
+                expect(doc.root.contents().length).to.equal(2);
+                expect(returned.sameNode(doc.root.contents()[0])).to.equal(true, 'modified node returned');
+                expect(textNode.getText()).to.equal('text1text2');
+            });
+
+            it('merges text nodes on after element node', function() {
+                var doc = getDocumentFromXML('<root><div></div>text1</root>'),
+                    textNode = doc.root.contents()[1],
+                    div = doc.root.contents()[0],
+                    returned;
+                returned = div.after({text: 'text2'});
+                expect(doc.root.contents().length).to.equal(2);
+                expect(returned.sameNode(doc.root.contents()[1])).to.equal(true, 'modified node returned');
+                expect(textNode.getText()).to.equal('text2text1');
+            });
+        });
+
         it('wraps element node with another element node', function() {
             var node = elementNodeFromXML('<div></div>'),
                 wrapper = elementNodeFromXML('<wrapper></wrapper>');