smartxml: first take on document fragments
authorAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Thu, 17 Apr 2014 08:59:48 +0000 (10:59 +0200)
committerAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Wed, 23 Apr 2014 11:05:03 +0000 (13:05 +0200)
src/smartxml/fragments.js [new file with mode: 0644]
src/smartxml/fragments.test.js [new file with mode: 0644]
src/smartxml/smartxml.js

diff --git a/src/smartxml/fragments.js b/src/smartxml/fragments.js
new file mode 100644 (file)
index 0000000..9bb92cc
--- /dev/null
@@ -0,0 +1,120 @@
+define(function(require) {
+    
+'use strict';
+
+var $ = require('libs/jquery'),
+    _ = require('libs/underscore');
+
+
+var Fragment = function(document) {
+    this.document = document;
+};
+$.extend(Fragment.prototype, {
+    isValid: function() {
+        return false;
+    }
+});
+
+
+var NodeFragment = function(document, params) {
+    Fragment.call(this, document);
+    this.node = params.node;
+};
+NodeFragment.prototype = Object.create(Fragment.prototype);
+$.extend(NodeFragment.prototype, {
+    isValid: function() {
+        return this.document.containsNode(this.node);
+    }
+});
+
+
+var CaretFragment = function(document, params) {
+    NodeFragment.call(this, document, params);
+    this.offset = params.offset;
+
+};
+CaretFragment.prototype = Object.create(NodeFragment.prototype);
+$.extend(CaretFragment.prototype, {
+    isValid: function() {
+        /* globals Node */
+        return NodeFragment.prototype.isValid.call(this) &&
+                this.node.nodeType === Node.TEXT_NODE &&
+                _.isNumber(this.offset);
+    }
+});
+
+
+
+var RangeFragment = function(document, params) {
+    Fragment.call(this, document);
+
+    if(params.node1.sameNode(params.node2)) {
+        this.startNode = this.endNode = params.node1;
+    } else {
+        /*jshint bitwise: false*/
+        /* globals Node */
+        var node1First = params.node1.nativeNode.compareDocumentPosition(params.node2.nativeNode) & Node.DOCUMENT_POSITION_FOLLOWING;
+        (node1First ? ['start', 'end'] : ['end','start']).forEach(function(prefix, idx) {
+            this[prefix + 'Node'] = params['node'+(idx+1)];
+        }.bind(this));
+    }
+};
+RangeFragment.prototype = Object.create(Fragment.prototype);
+$.extend(RangeFragment.prototype, {
+    isValid: function() {
+        return this.document.containsNode(this.startNode) && this.document.containsNode(this.endNode);
+    },
+    hasSiblingBoundries: function() {
+        return this.isValid() && this.startNode.isSiblingOf(this.endNode);
+    },
+    boundriesSiblingParents: function() {
+        return this.startNode.document.getSiblingParents({
+            node1: this.startNode,
+            node2: this.endNode
+        });
+    },
+    getCommonParent: function() {
+        var siblingParents = this.boundriesSiblingParents();
+        if(siblingParents) {
+            return siblingParents.node1.parent();
+        }
+    },
+});
+
+var TextRangeFragment = function(document, params) {
+    var orderChanged;
+
+    RangeFragment.call(this, document, params);
+
+    if(this.startNode.sameNode(this.endNode)) {
+        this.startOffset = Math.min(params.offset1, params.offset2);
+        this.endOffset = Math.max(params.offset1, params.offset2);
+    } else {
+        orderChanged =  !params.node1.sameNode(this.startNode);
+        this.startOffset = orderChanged ? params.offset2 : params.offset1;
+        this.endOffset = orderChanged ? params.offset1 : params.offset2;
+    }
+};
+TextRangeFragment.prototype = Object.create(RangeFragment.prototype);
+$.extend(TextRangeFragment.prototype, {
+    isValid: function() {
+        return RangeFragment.prototype.isValid.call(this) &&
+            _.isNumber(this.startOffset) &&
+            _.isNumber(this.endOffset);
+    }
+});
+
+var FragmentTypes = {
+    Fragment: Fragment,
+    NodeFragment: NodeFragment,
+    CaretFragment: CaretFragment,
+    RangeFragment: RangeFragment,
+    TextRangeFragment: TextRangeFragment
+};
+_.values(FragmentTypes).forEach(function(Type) {
+    $.extend(Type.prototype, FragmentTypes);
+});
+
+return FragmentTypes;
+
+});
\ No newline at end of file
diff --git a/src/smartxml/fragments.test.js b/src/smartxml/fragments.test.js
new file mode 100644 (file)
index 0000000..8cd6a58
--- /dev/null
@@ -0,0 +1,82 @@
+define(function(require) {
+    
+'use strict';
+/* global describe, it */
+/* jshint expr:true */
+
+
+var chai = require('libs/chai'),
+    smartxml = require('./smartxml.js');
+
+
+var expect = chai.expect;
+
+var getDocumentFromXML = function(xml) {
+    return smartxml.documentFromXML(xml);
+};
+
+describe('Fragments API', function() {
+    describe('node fragment', function() {
+        it('describes a single node', function() {
+            var doc = getDocumentFromXML('<section></section');
+
+            var fragment = doc.createFragment(doc.NodeFragment, {node:doc.root});
+            expect(fragment instanceof fragment.NodeFragment).to.be.true;
+            expect(fragment.node.sameNode(doc.root)).to.be.true;
+        });
+    });
+
+    describe('caret fragment', function() {
+        it('describes place in a text', function() {
+            var doc = getDocumentFromXML('<section>Alice</section>');
+
+            var fragment = doc.createFragment(doc.CaretFragment, {node: doc.root.contents()[0], offset: 1});
+
+            expect(fragment instanceof fragment.CaretFragment).to.be.true;
+            expect(fragment instanceof fragment.NodeFragment).to.be.true;
+            expect(fragment.node.getText()).to.equal('Alice');
+            expect(fragment.offset).to.equal(1);
+        });
+    });
+
+    describe('text range fragment', function() {
+        it('describes fragment of a text node', function() {
+            var doc = getDocumentFromXML('<section>Alice</section>'),
+                textNode = doc.root.contents()[0];
+
+            var fragment = doc.createFragment(doc.TextRangeFragment, {
+                node1: textNode,
+                offset1: 4,
+                node2: textNode,
+                offset2: 1
+            });
+
+            expect(fragment instanceof fragment.TextRangeFragment).to.be.true;
+            expect(fragment instanceof fragment.RangeFragment).to.be.true;
+            expect(fragment.startNode.getText()).to.equal('Alice');
+            expect(fragment.startOffset).to.equal(1);
+            expect(fragment.endNode.getText()).to.equal('Alice');
+            expect(fragment.endOffset).to.equal(4);
+        });
+        it('describes text spanning multiple nodes', function() {
+            var doc = getDocumentFromXML('<section>Alice <span>has</span> a cat!</section>'),
+                textNode1 = doc.root.contents()[0],
+                textNode2 = doc.root.contents()[2];
+
+            var fragment = doc.createFragment(doc.TextRangeFragment, {
+                node1: textNode2,
+                offset1: 4,
+                node2: textNode1,
+                offset2: 1
+            });
+
+            expect(fragment instanceof fragment.TextRangeFragment).to.be.true;
+            expect(fragment.startNode.getText()).to.equal('Alice ');
+            expect(fragment.startOffset).to.equal(1);
+            expect(fragment.endNode.getText()).to.equal(' a cat!');
+            expect(fragment.endOffset).to.equal(4);
+        });
+    });
+});
+
+});
\ No newline at end of file
index c8799f1..08897cc 100644 (file)
@@ -4,8 +4,9 @@ define([
     'libs/backbone',
     'smartxml/events',
     'smartxml/transformations',
-    'smartxml/core'
-], function($, _, Backbone, events, transformations, coreTransformations) {
+    'smartxml/core',
+    'smartxml/fragments'
+], function($, _, Backbone, events, transformations, coreTransformations, fragments) {
     
 'use strict';
 /* globals Node */
@@ -299,7 +300,7 @@ var Document = function(xml, extensions) {
     this.loadXML(xml);
 };
 
-$.extend(Document.prototype, Backbone.Events, {
+$.extend(Document.prototype, Backbone.Events, fragments, {
     ElementNodeFactory: ElementNode,
     TextNodeFactory: TextNode,
 
@@ -638,6 +639,13 @@ $.extend(Document.prototype, Backbone.Events, {
             }
             return $document[0];
         }, configurable: true});
+    },
+
+    createFragment: function(Type, params) {
+        if(!Type.prototype instanceof fragments.Fragment) {
+            throw new Error('Can\'t create a fragment: `Type` is not a valid Fragment');
+        }
+        return new Type(this, params);
     }
 });