From: Aleksander Ɓukasz Date: Thu, 17 Apr 2014 08:59:48 +0000 (+0200) Subject: smartxml: first take on document fragments X-Git-Url: https://git.mdrn.pl/fnpeditor.git/commitdiff_plain/b54134dba23e4a09a31655ff6697eacb915659bb?ds=inline smartxml: first take on document fragments --- diff --git a/src/smartxml/fragments.js b/src/smartxml/fragments.js new file mode 100644 index 0000000..9bb92cc --- /dev/null +++ b/src/smartxml/fragments.js @@ -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 index 0000000..8cd6a58 --- /dev/null +++ b/src/smartxml/fragments.test.js @@ -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('
Alice
'); + + 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('
Alice
'), + 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('
Alice has a cat!
'), + 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 diff --git a/src/smartxml/smartxml.js b/src/smartxml/smartxml.js index c8799f1..08897cc 100644 --- a/src/smartxml/smartxml.js +++ b/src/smartxml/smartxml.js @@ -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); } });