소스 검색

Merge branch 'master' into t/275

Szymon Kupś 9 년 전
부모
커밋
d3f355e769
27개의 변경된 파일1150개의 추가작업 그리고 102개의 파일을 삭제
  1. 15 0
      packages/ckeditor5-engine/src/treecontroller/datacontroller.js
  2. 17 0
      packages/ckeditor5-engine/src/treecontroller/editingcontroller.js
  3. 0 1
      packages/ckeditor5-engine/src/treemodel/delta/insertdelta.js
  4. 111 0
      packages/ckeditor5-engine/src/treemodel/documentfragment.js
  5. 7 2
      packages/ckeditor5-engine/src/treemodel/element.js
  6. 26 11
      packages/ckeditor5-engine/src/treemodel/liveposition.js
  7. 2 2
      packages/ckeditor5-engine/src/treemodel/node.js
  8. 10 1
      packages/ckeditor5-engine/src/treemodel/nodelist.js
  9. 4 9
      packages/ckeditor5-engine/src/treemodel/operation/attributeoperation.js
  10. 21 23
      packages/ckeditor5-engine/src/treemodel/position.js
  11. 4 2
      packages/ckeditor5-engine/src/treemodel/range.js
  12. 67 0
      packages/ckeditor5-engine/src/treemodel/textfragment.js
  13. 1 1
      packages/ckeditor5-engine/src/treemodel/treewalker.js
  14. 125 0
      packages/ckeditor5-engine/src/treeview/documentfragment.js
  15. 106 34
      packages/ckeditor5-engine/src/treeview/domconverter.js
  16. 1 1
      packages/ckeditor5-engine/src/treeview/node.js
  17. 4 3
      packages/ckeditor5-engine/src/treeview/writer.js
  18. 1 1
      packages/ckeditor5-engine/tests/dataprocessor/htmldataprocessor.js
  19. 132 0
      packages/ckeditor5-engine/tests/treemodel/documentfragment.js
  20. 31 0
      packages/ckeditor5-engine/tests/treemodel/element.js
  21. 8 0
      packages/ckeditor5-engine/tests/treemodel/liveposition.js
  22. 13 0
      packages/ckeditor5-engine/tests/treemodel/nodelist.js
  23. 10 3
      packages/ckeditor5-engine/tests/treemodel/position.js
  24. 76 3
      packages/ckeditor5-engine/tests/treemodel/textfragment.js
  25. 199 0
      packages/ckeditor5-engine/tests/treeview/documentfragment.js
  26. 153 0
      packages/ckeditor5-engine/tests/treeview/domconverter.js
  27. 6 5
      packages/ckeditor5-engine/tests/treeview/writer/remove.js

+ 15 - 0
packages/ckeditor5-engine/src/treecontroller/datacontroller.js

@@ -0,0 +1,15 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+export default class DataController {
+	constructor( modelDocument, dataProcessor ) {
+		this.model = modelDocument;
+		this.processor = dataProcessor;
+	}
+
+	destroy() {}
+}

+ 17 - 0
packages/ckeditor5-engine/src/treecontroller/editingcontroller.js

@@ -0,0 +1,17 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import TreeView from '../treeview/treeview.js';
+
+export default class EditingController {
+	constructor( modelDocument ) {
+		this.model = modelDocument;
+		this.view = new TreeView();
+	}
+
+	destroy() {}
+}

+ 0 - 1
packages/ckeditor5-engine/src/treemodel/delta/insertdelta.js

@@ -67,7 +67,6 @@ export default class InsertDelta extends Delta {
  * @method engine.treeModel.Batch#insert
  * @param {engine.treeModel.Position} position Position of insertion.
  * @param {engine.treeModel.NodeSet} nodes The list of nodes to be inserted.
- * List of nodes can be of any type accepted by the {@link engine.treeModel.NodeList} constructor.
  */
 register( 'insert', function( position, nodes ) {
 	const delta = new InsertDelta();

+ 111 - 0
packages/ckeditor5-engine/src/treemodel/documentfragment.js

@@ -0,0 +1,111 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import NodeList from './nodelist.js';
+
+/**
+ * DocumentFragment represents a part of Tree Model which does not have a common root but it's top level nodes
+ * can be seen as siblings.
+ *
+ * @memberOf engine.treeModel
+ */
+export default class DocumentFragment {
+	/**
+	 * Creates empty DocumentFragment.
+	 *
+	 * @param {engine.treeModel.NodeSet} children List of nodes contained inside the DocumentFragment.
+	 */
+	constructor( children ) {
+		/**
+		 * List of nodes contained inside the DocumentFragment.
+		 *
+		 * @protected
+		 * @member {engine.treeModel.NodeSet} engine.treeModel.DocumentFragment#_children
+		 */
+		this._children = new NodeList();
+
+		if ( children ) {
+			this.insertChildren( 0, children );
+		}
+	}
+
+	/**
+	 * Gets child at the given index.
+	 *
+	 * @param {Number} index Index of child.
+	 * @returns {engine.treeModel.Node} Child node.
+	 */
+	getChild( index ) {
+		return this._children.get( index );
+	}
+
+	/**
+	 * Gets the number of top-level elements of DocumentFragment.
+	 *
+	 * @returns {Number} The number of top-level elements.
+	 */
+	getChildCount() {
+		return this._children.length;
+	}
+
+	/**
+	 * Gets index of the given child node.
+	 *
+	 * @param {engine.treeModel.Node} node Child node.
+	 * @returns {Number} Index of the child node.
+	 */
+	getChildIndex( node ) {
+		return this._children.indexOf( node );
+	}
+
+	/**
+	 * Inserts a child node or a list of child nodes at the end of this DocumentFragment.
+	 *
+	 * @param {engine.treeModel.NodeSet} nodes The list of nodes to be inserted.
+	 */
+	appendChildren( nodes ) {
+		this.insertChildren( this.getChildCount(), nodes );
+	}
+
+	/**
+	 * Inserts a list of child nodes on the given index and sets the parent of these nodes to this DocumentFragment.
+	 *
+	 * @param {Number} index Position where nodes should be inserted.
+	 * @param {engine.treeModel.NodeSet} nodes The list of nodes to be inserted.
+	 */
+	insertChildren( index, nodes ) {
+		let nodeList = new NodeList( nodes );
+
+		for ( let node of nodeList._nodes ) {
+			node.parent = this;
+		}
+
+		// Clean original DocumentFragment so it won't contain nodes that were added somewhere else.
+		if ( nodes instanceof DocumentFragment ) {
+			nodes._children = new NodeList();
+		}
+
+		this._children.insert( index, nodeList );
+	}
+
+	/**
+	 * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
+	 *
+	 * @param {Number} index Position of the first node to remove.
+	 * @param {Number} [howMany=1] Number of nodes to remove.
+	 * @returns {engine.treeModel.NodeList} The list of removed nodes.
+	 */
+	removeChildren( index, howMany = 1 ) {
+		let nodeList = this._children.remove( index, howMany );
+
+		for ( let node of nodeList._nodes ) {
+			node.parent = null;
+		}
+
+		return nodeList;
+	}
+}

+ 7 - 2
packages/ckeditor5-engine/src/treemodel/element.js

@@ -7,6 +7,7 @@
 
 import Node from './node.js';
 import NodeList from './nodelist.js';
+import DocumentFragment from './documentfragment.js';
 import utils from '../../utils/utils.js';
 
 /**
@@ -20,7 +21,7 @@ export default class Element extends Node {
 	 *
 	 * @param {String} name Node name.
 	 * @param {Iterable} attrs Iterable collection of attributes.
-	 * @param {engine.treeModel.NodeSet} children List of nodes to be inserted
+	 * @param {engine.treeModel.NodeSet} children List of nodes to be inserted.
 	 * into created element. List of nodes can be of any type accepted by the {@link engine.treeModel.NodeList} constructor.
 	 */
 	constructor( name, attrs, children ) {
@@ -97,7 +98,6 @@ export default class Element extends Node {
 	 *
 	 * @param {Number} index Position where nodes should be inserted.
 	 * @param {engine.treeModel.NodeSet} nodes The list of nodes to be inserted.
-	 * The list of nodes can be of any type accepted by the {@link engine.treeModel.NodeList} constructor.
 	 */
 	insertChildren( index, nodes ) {
 		let nodeList = new NodeList( nodes );
@@ -106,6 +106,11 @@ export default class Element extends Node {
 			node.parent = this;
 		}
 
+		// Clean original DocumentFragment so it won't contain nodes that were added somewhere else.
+		if ( nodes instanceof DocumentFragment ) {
+			nodes._children = new NodeList();
+		}
+
 		this._children.insert( index, nodeList );
 	}
 

+ 26 - 11
packages/ckeditor5-engine/src/treemodel/liveposition.js

@@ -5,13 +5,18 @@
 
 'use strict';
 
+import RootElement from './rootelement.js';
 import Position from './position.js';
 import Range from './range.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import utils from '../../utils/utils.js';
+import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
- * LivePosition is a position in the Tree Model that updates itself as the tree changes. It may be used as a bookmark.
+ * LivePosition is a position in {@link engine.treeModel.Document Document} that updates itself as Document is changed
+ * through operations. It may be used as a bookmark in the Document.
+ * **Note:** Contrary to {@link engine.treeModel.Position}, LivePosition works only in roots that are
+ * {@link engine.treeModel.RootElement}. If {@link engine.treeModel.DocumentFragment} is passed, error will be thrown.
  * **Note:** Be very careful when dealing with LivePosition. Each LivePosition instance bind events that might
  * have to be unbound. Use {@link engine.treeModel.LivePosition#detach} whenever you don't need LivePosition anymore.
  *
@@ -25,10 +30,19 @@ export default class LivePosition extends Position {
 	 * @see engine.treeModel.Position
 	 * @param {engine.treeModel.RootElement} root
 	 * @param {Array.<Number>} path
-	 * @param {engine.treeModel.PositionStickiness} [stickiness] Defaults to `'STICKS_TO_NEXT'`. See
-	 *  {@link engine.treeModel.LivePosition#stickiness}.
+	 * @param {engine.treeModel.PositionStickiness} [stickiness] Defaults to `'STICKS_TO_NEXT'`.
+	 * See {@link engine.treeModel.LivePosition#stickiness}.
 	 */
 	constructor( root, path, stickiness ) {
+		if ( !( root instanceof RootElement ) ) {
+			/**
+			 * LivePosition root has to be an instance of RootElement.
+			 *
+			 * @error liveposition-root-not-rootelement
+			 */
+			throw new CKEditorError( 'liveposition-root-not-rootelement: LivePosition root has to be an instance of RootElement.' );
+		}
+
 		super( root, path );
 
 		/**
@@ -38,15 +52,16 @@ export default class LivePosition extends Position {
 		 * position is same as LivePosition.
 		 *
 		 * Examples:
-		 * Insert:
-		 * Position is at | and we insert at the same position, marked as ^:
-		 * - | sticks to previous node: `<p>f|^oo</p>` => `<p>f|baroo</p>`
-		 * - | sticks to next node: `<p>f^|oo</p>` => `<p>fbar|oo</p>`
 		 *
-		 * Move:
-		 * Position is at | and range [ ] is moved to position ^:
-		 * - | sticks to previous node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f|</p><p>booar</p>`
-		 * - | sticks to next node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f</p><p>b|ooar</p>`
+		 *		Insert:
+		 *		Position is at | and we insert at the same position, marked as ^:
+		 *		- | sticks to previous node: `<p>f|^oo</p>` => `<p>f|baroo</p>`
+		 *		- | sticks to next node: `<p>f^|oo</p>` => `<p>fbar|oo</p>`
+		 *
+		 *		Move:
+		 *		Position is at | and range [ ] is moved to position ^:
+		 *		- | sticks to previous node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f|</p><p>booar</p>`
+		 *		- | sticks to next node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f</p><p>b|ooar</p>`
 		 *
 		 * @member {engine.treeModel.PositionStickiness} engine.treeModel.LivePosition#stickiness
 		 */

+ 2 - 2
packages/ckeditor5-engine/src/treemodel/node.js

@@ -25,10 +25,10 @@ export default class Node {
 	 */
 	constructor( attrs ) {
 		/**
-		 * Parent element. Null by default. Set by {@link engine.treeModel.Element#insertChildren}.
+		 * Element or DocumentFragment that is a parent of this node.
 		 *
 		 * @readonly
-		 * @member {engine.treeModel.Element|null} engine.treeModel.Node#parent
+		 * @member {engine.treeModel.Element|engine.treeModel.DocumentFragment|null} engine.treeModel.Node#parent
 		 */
 		this.parent = null;
 

+ 10 - 1
packages/ckeditor5-engine/src/treemodel/nodelist.js

@@ -7,6 +7,7 @@
 
 import CharacterProxy from './characterproxy.js';
 import Text from './text.js';
+import DocumentFragment from './documentfragment.js';
 import utils from '../../utils/utils.js';
 import clone from '../../utils/lib/lodash/clone.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
@@ -27,6 +28,11 @@ class NodeListText extends Text {
 	constructor( text, attrs ) {
 		super( text, attrs );
 
+		/**
+		 * Element that contains character nodes represented by this NodeListText.
+		 *
+		 * @type {engine.treeModel.Element|engine.treeModel.DocumentFragment|null}
+		 */
 		this.parent = null;
 	}
 
@@ -103,6 +109,8 @@ export default class NodeList {
 		if ( nodes instanceof NodeList ) {
 			// We do not clone anything.
 			return nodes;
+		} else if ( nodes instanceof DocumentFragment ) {
+			return nodes._children;
 		}
 
 		/**
@@ -449,5 +457,6 @@ export default class NodeList {
  * point to the same nodes.
  * * Iterable collection of above items will be iterated over and all items will be added to the node list.
  *
- * @typedef {engine.treeModel.Node|engine.treeModel.Text|String|engine.treeModel.NodeList|Iterable} engine.treeModel.NodeSet
+ * @typedef {engine.treeModel.Node|engine.treeModel.Text|String|engine.treeModel.NodeList|engine.treeModel.DocumentFragment|Iterable}
+ * engine.treeModel.NodeSet
  */

+ 4 - 9
packages/ckeditor5-engine/src/treemodel/operation/attributeoperation.js

@@ -120,15 +120,10 @@ export default class AttributeOperation extends Operation {
 				);
 			}
 
-			if ( item instanceof TextFragment ) {
-				item.commonParent._children.setAttribute( item.first.getIndex(), item.text.length, this.key, this.newValue );
-			}
-			else {
-				if ( this.newValue !== null ) {
-					item.setAttribute( this.key, this.newValue );
-				} else {
-					item.removeAttribute( this.key );
-				}
+			if ( this.newValue !== null ) {
+				item.setAttribute( this.key, this.newValue );
+			} else {
+				item.removeAttribute( this.key );
 			}
 		}
 

+ 21 - 23
packages/ckeditor5-engine/src/treemodel/position.js

@@ -6,9 +6,9 @@
 'use strict';
 
 import RootElement from './rootelement.js';
-import CKEditorError from '../../utils/ckeditorerror.js';
 import last from '../../utils/lib/lodash/last.js';
 import utils from '../../utils/utils.js';
+import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
  * Position in the tree. Position is always located before or after a node.
@@ -20,24 +20,22 @@ export default class Position {
 	/**
 	 * Creates a position.
 	 *
-	 * @param {engine.treeModel.RootElement} root Root element for the path. Note that this element can not have a parent.
-	 * @param {Array.<Number>} path Position path. Must contain at least one item. See {@link #path} property for more information.
+	 * @param {engine.treeModel.RootElement|engine.treeModel.DocumentFragment} root Root element for the position path.
+	 * @param {Array.<Number>} path Position path. See {@link engine.treeModel.Position#path} property for more information.
 	 */
 	constructor( root, path ) {
-		if ( !( root instanceof RootElement ) ) {
+		if ( !( root instanceof RootElement ) && !( root instanceof DocumentFragment ) ) {
 			/**
-			 * Position root has to be an instance of RootElement.
+			 * Position root invalid.
 			 *
-			 * @error position-root-not-rootelement
-			 * @param root
+			 * @error position-root-invalid.
 			 */
-			throw new CKEditorError( 'position-root-not-rootelement: Position root has to be an instance of RootElement.', { root: root } );
+			throw new CKEditorError( 'position-root-invalid: Position root invalid.' );
 		}
-
 		/**
-		 * Root element for the path. Note that this element can not have a parent.
+		 * Root element for the position path.
 		 *
-		 * @member {engine.treeModel.RootElement} engine.treeModel.Position#root
+		 * @member {engine.treeModel.RootElement|engine.treeModel.DocumentFragment} engine.treeModel.Position#root
 		 */
 		this.root = root;
 
@@ -52,19 +50,19 @@ export default class Position {
 		}
 
 		/**
-		 * Position of the node it the tree. For example:
+		 * Position of the node it the tree. Must contain at least one item. For example:
 		 *
-		 * root
-		 *  |- p         Before: [ 0 ]       After: [ 1 ]
-		 *  |- ul        Before: [ 1 ]       After: [ 2 ]
-		 *     |- li     Before: [ 1, 0 ]    After: [ 1, 1 ]
-		 *     |  |- f   Before: [ 1, 0, 0 ] After: [ 1, 0, 1 ]
-		 *     |  |- o   Before: [ 1, 0, 1 ] After: [ 1, 0, 2 ]
-		 *     |  |- o   Before: [ 1, 0, 2 ] After: [ 1, 0, 3 ]
-		 *     |- li     Before: [ 1, 1 ]    After: [ 1, 2 ]
-		 *        |- b   Before: [ 1, 1, 0 ] After: [ 1, 1, 1 ]
-		 *        |- a   Before: [ 1, 1, 1 ] After: [ 1, 1, 2 ]
-		 *        |- r   Before: [ 1, 1, 2 ] After: [ 1, 1, 3 ]
+		 *		 root
+		 *		  |- p         Before: [ 0 ]       After: [ 1 ]
+		 *		  |- ul        Before: [ 1 ]       After: [ 2 ]
+		 *		     |- li     Before: [ 1, 0 ]    After: [ 1, 1 ]
+		 *		     |  |- f   Before: [ 1, 0, 0 ] After: [ 1, 0, 1 ]
+		 *		     |  |- o   Before: [ 1, 0, 1 ] After: [ 1, 0, 2 ]
+		 *		     |  |- o   Before: [ 1, 0, 2 ] After: [ 1, 0, 3 ]
+		 *		     |- li     Before: [ 1, 1 ]    After: [ 1, 2 ]
+		 *		        |- b   Before: [ 1, 1, 0 ] After: [ 1, 1, 1 ]
+		 *		        |- a   Before: [ 1, 1, 1 ] After: [ 1, 1, 2 ]
+		 *		        |- r   Before: [ 1, 1, 2 ] After: [ 1, 1, 3 ]
 		 *
 		 * @member {Array.<Number>} engine.treeModel.Position#path
 		 */

+ 4 - 2
packages/ckeditor5-engine/src/treemodel/range.js

@@ -59,9 +59,11 @@ export default class Range {
 	}
 
 	/**
-	 * Range root element. Equals to the root of start position (which should be same as root of end position).
+	 * Range root element.
 	 *
-	 * @type {engine.treeModel.RootElement}
+	 * Equals to the root of start position (which should be same as root of end position).
+	 *
+	 * @type {engine.treeModel.RootElement|engine.treeModel.DocumentFragment}
 	 */
 	get root() {
 		return this.start.root;

+ 67 - 0
packages/ckeditor5-engine/src/treemodel/textfragment.js

@@ -6,12 +6,18 @@
 'use strict';
 
 import CharacterProxy from './characterproxy.js';
+import utils from '../../utils/utils.js';
 
 /**
  * TextFragment is an aggregator for multiple CharacterProxy instances that are placed next to each other in
  * tree model, in the same parent, and all have same attributes set. Instances of this class are created and returned
  * in various algorithms that "merge characters" (see {@link engine.treeModel.TreeWalker}, {@link engine.treeModel.Range}).
  *
+ * **Note:** TextFragment instances are created on the fly basing on the current state of tree model and attributes
+ * set on characters. Because of this it is highly unrecommended to store references to TextFragment instances
+ * because they might get invalidated due to operations on Document. This is especially true when you change
+ * attributes of TextFragment.
+ *
  * Difference between {@link engine.treeModel.TextFragment} and {@link engine.treeModel.Text} is that the former is a set of
  * nodes taken from tree model, while {@link engine.treeModel.Text} is simply a string with attributes set.
  *
@@ -105,4 +111,65 @@ export default class TextFragment {
 	getAttributes() {
 		return this.first.getAttributes();
 	}
+
+	/**
+	 * Sets attribute on the text fragment. If attribute with the same key already is set, it overwrites its values.
+	 *
+	 * **Note:** Changing attributes of text fragment affects document state. This TextFragment instance properties
+	 * will be refreshed, but other may get invalidated. It is highly unrecommended to store references to TextFragment instances.
+	 *
+	 * @param {String} key Key of attribute to set.
+	 * @param {*} value Attribute value.
+	 */
+	setAttribute( key, value ) {
+		let index = this.first.getIndex();
+
+		this.commonParent._children.setAttribute( this.first.getIndex(), this.text.length, key, value );
+
+		this.first = this.commonParent.getChild( index );
+		this.last = this.getCharAt( this.text.length - 1 );
+	}
+
+	/**
+	 * Removes all attributes from the text fragment and sets given attributes.
+	 *
+	 * **Note:** Changing attributes of text fragment affects document state. This TextFragment instance properties
+	 * will be refreshed, but other may get invalidated. It is highly unrecommended to store references to TextFragment instances.
+	 *
+	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
+	 * See {@link engine.treeModel.TextFragment#getAttributes}.
+	 */
+	setAttributesTo( attrs ) {
+		let attrsMap = utils.toMap( attrs );
+
+		this.clearAttributes();
+
+		for ( let attr of attrsMap ) {
+			this.setAttribute( attr[ 0 ], attr[ 1 ] );
+		}
+	}
+
+	/**
+	 * Removes an attribute with given key from the text fragment.
+	 *
+	 * **Note:** Changing attributes of text fragment affects document state. This TextFragment instance properties
+	 * will be refreshed, but other may get invalidated. It is highly unrecommended to store references to TextFragment instances.
+	 *
+	 * @param {String} key Key of attribute to remove.
+	 */
+	removeAttribute( key ) {
+		this.setAttribute( key, null );
+	}
+
+	/**
+	 * Removes all attributes from the text fragment.
+	 *
+	 * **Note:** Changing attributes of text fragment affects document state. This TextFragment instance properties
+	 * will be refreshed, but other may get invalidated. It is highly unrecommended to store references to TextFragment instances.
+	 */
+	clearAttributes() {
+		for ( let attr of this.getAttributes() ) {
+			this.removeAttribute( attr[ 0 ] );
+		}
+	}
 }

+ 1 - 1
packages/ckeditor5-engine/src/treemodel/treewalker.js

@@ -104,7 +104,7 @@ export default class TreeWalker {
 		 * Parent of the most recently visited node. Cached for optimization purposes.
 		 *
 		 * @private
-		 * @member {engine.treeModel.Element} engine.treeModel.TreeWalker#_visitedParent
+		 * @member {engine.treeModel.Element|engine.treeModel.DocumentFragment} engine.treeModel.TreeWalker#_visitedParent
 		 */
 		this._visitedParent = this.position.parent;
 	}

+ 125 - 0
packages/ckeditor5-engine/src/treeview/documentfragment.js

@@ -0,0 +1,125 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import utils from '../../utils/utils.js';
+
+/**
+ * DocumentFragment class.
+ *
+ * @memberOf engine.treeView
+ */
+export default class DocumentFragment {
+	/**
+	 * Creates new DocumentFragment instance.
+	 *
+	 * @param {engine.treeView.Node|Iterable.<engine.treeView.Node>} [children] List of nodes to be inserted into
+	 * created document fragment.
+	 */
+	constructor( children ) {
+		/**
+		 * Array of child nodes.
+		 *
+		 * @protected
+		 * @member {Array.<engine.treeView.Element>} engine.treeView.DocumentFragment#_children
+		 */
+		this._children = [];
+
+		if ( children ) {
+			this.insertChildren( 0, children );
+		}
+	}
+
+	/**
+	 * {@link engine.treeView.DocumentFragment#insertChildren Insert} a child node or a list of child nodes at the end
+	 * and sets the parent of these nodes to this fragment.
+	 *
+	 * @param {engine.treeView.Node|Iterable.<engine.treeView.Node>} nodes Node or the list of nodes to be inserted.
+	 * @returns {Number} Number of appended nodes.
+	 */
+	appendChildren( nodes ) {
+		return this.insertChildren( this.getChildCount(), nodes );
+	}
+
+	/**
+	 * Gets child at the given index.
+	 *
+	 * @param {Number} index Index of child.
+	 * @returns {engine.treeView.Node} Child node.
+	 */
+	getChild( index ) {
+		return this._children[ index ];
+	}
+
+	/**
+	 * Gets the number of elements in fragment.
+	 *
+	 * @returns {Number} The number of elements.
+	 */
+	getChildCount() {
+		return this._children.length;
+	}
+
+	/**
+	 * Gets index of the given child node. Returns `-1` if child node is not found.
+	 *
+	 * @param {engine.treeView.Node} node Child node.
+	 * @returns {Number} Index of the child node.
+	 */
+	getChildIndex( node ) {
+		return this._children.indexOf( node );
+	}
+
+	/**
+	 * Gets child nodes iterator.
+	 *
+	 * @returns {Iterable.<engine.treeView.Node>} Child nodes iterator.
+	 */
+	getChildren() {
+		return this._children[ Symbol.iterator ]();
+	}
+
+	/**
+	 * Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to
+	 * this fragment.
+	 *
+	 * @param {Number} index Position where nodes should be inserted.
+	 * @param {engine.treeView.Node|Iterable.<engine.treeView.Node>} nodes Node or list of nodes to be inserted.
+	 * @returns {Number} Number of inserted nodes.
+	 */
+	insertChildren( index, nodes ) {
+		let count = 0;
+
+		if ( !utils.isIterable( nodes ) ) {
+			nodes = [ nodes ];
+		}
+
+		for ( let node of nodes ) {
+			node.parent = this;
+
+			this._children.splice( index, 0, node );
+			index++;
+			count++;
+		}
+
+		return count;
+	}
+
+	/**
+	 * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
+	 *
+	 * @param {Number} index Number of the first node to remove.
+	 * @param {Number} [howMany=1] Number of nodes to remove.
+	 * @returns {Array.<engine.treeView.Node>} The array of removed nodes.
+	 */
+	removeChildren( index, howMany = 1 ) {
+		for ( let i = index; i < index + howMany; i++ ) {
+			this._children[ i ].parent = null;
+		}
+
+		return this._children.splice( index, howMany );
+	}
+}

+ 106 - 34
packages/ckeditor5-engine/src/treeview/domconverter.js

@@ -7,6 +7,7 @@
 
 import ViewText from './text.js';
 import ViewElement from './element.js';
+import ViewDocumentFragment from './documentfragment.js';
 
 /**
  * DomConverter is a set of tools to do transformations between DOM nodes and view nodes. It also handles
@@ -52,8 +53,8 @@ export default class DomConverter {
 
 	/**
 	 * Binds DOM and View elements, so it will be possible to get corresponding elements using
-	 * {@link engine.treeView.DomConverter#getCorrespondingViewElement} and
-	 * {@link engine.treeView.DomConverter#getCorespondingDOMElement}.
+	 * {@link engine.treeView.DomConverter#getCorrespondingViewElement getCorrespondingViewElement} and
+	 * {@link engine.treeView.DomConverter#getCorrespondingDomElement getCorrespondingDomElement}.
 	 *
 	 * @param {HTMLElement} domElement DOM element to bind.
 	 * @param {engine.treeView.Element} viewElement View element to bind.
@@ -63,6 +64,19 @@ export default class DomConverter {
 		this._viewToDomMapping.set( viewElement, domElement );
 	}
 
+	/**
+	 * Binds DOM and View document fragments, so it will be possible to get corresponding document fragments using
+	 * {@link engine.treeView.DomConverter#getCorrespondingViewDocumentFragment getCorrespondingViewDocumentFragment} and
+	 * {@link engine.treeView.DomConverter#getCorrespondingDomDocumentFragment getCorrespondingDomDocumentFragment}.
+	 *
+	 * @param {DocumentFragment} domFragment DOM document fragment to bind.
+	 * @param {engine.treeView.DocumentFragment} viewFragment View document fragment to bind.
+	 */
+	bindDocumentFragments( domFragment, viewFragment ) {
+		this._domToViewMapping.set( domFragment, viewFragment );
+		this._viewToDomMapping.set( viewFragment, domFragment );
+	}
+
 	/**
 	 * Compares DOM and View nodes. Elements are same when they are bound. Text nodes are same when they have the same
 	 * text data. Nodes need to have corresponding types. In all other cases nodes are different.
@@ -86,15 +100,15 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Converts view to DOM. For all text nodes and not bound elements new elements will be created. For bound
-	 * elements function will return corresponding elements.
+	 * Converts view to DOM. For all text nodes, not bound elements and document fragments new items will
+	 * be created. For bound elements and document fragments function will return corresponding items.
 	 *
-	 * @param {engine.treeView.Node} viewNode View node to transform.
+	 * @param {engine.treeView.Node|engine.treeView.DocumentFragment} viewNode View node or document fragment to transform.
 	 * @param {document} domDocument Document which will be used to create DOM nodes.
 	 * @param {Object} [options] Conversion options.
 	 * @param {Boolean} [options.bind=false] Determines whether new elements will be bound.
-	 * @param {Boolean} [options.withChildren=true] If true node's children will be converted too.
-	 * @returns {Node} Converted node.
+	 * @param {Boolean} [options.withChildren=true] If true node's and document fragment's children  will be converted too.
+	 * @returns {Node|DocumentFragment} Converted node or DocumentFragment.
 	 */
 	viewToDom( viewNode, domDocument, options ) {
 		if ( !options ) {
@@ -108,14 +122,27 @@ export default class DomConverter {
 				return this.getCorrespondingDom( viewNode );
 			}
 
-			const domElement = domDocument.createElement( viewNode.name );
+			let domElement;
 
-			if ( options.bind ) {
-				this.bindElements( domElement, viewNode );
-			}
+			if ( viewNode instanceof ViewDocumentFragment ) {
+				// Create DOM document fragment.
+				domElement = domDocument.createDocumentFragment();
+
+				if ( options.bind ) {
+					this.bindDocumentFragments( domElement, viewNode );
+				}
+			} else {
+				// Create DOM element.
+				domElement = domDocument.createElement( viewNode.name );
+
+				if ( options.bind ) {
+					this.bindElements( domElement, viewNode );
+				}
 
-			for ( let key of viewNode.getAttributeKeys() ) {
-				domElement.setAttribute( key, viewNode.getAttribute( key ) );
+				// Copy element's attributes.
+				for ( let key of viewNode.getAttributeKeys() ) {
+					domElement.setAttribute( key, viewNode.getAttribute( key ) );
+				}
 			}
 
 			if ( options.withChildren || options.withChildren === undefined ) {
@@ -129,14 +156,14 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Converts DOM to view. For all text nodes and not bound elements new elements will be created. For bound
-	 * elements function will return corresponding elements.
+	 * Converts DOM to view. For all text nodes, not bound elements and document fragments new items will
+	 * be created. For bound elements and document fragments function will return corresponding items.
 	 *
-	 * @param {Node} domNode DOM node to transform.
+	 * @param {Node|DocumentFragment} domNode DOM node or document fragment to transform.
 	 * @param {Object} [options] Conversion options.
 	 * @param {Boolean} [options.bind=false] Determines whether new elements will be bound.
-	 * @param {Boolean} [options.withChildren=true] It true node's children will be converted too.
-	 * @returns {engine.treeView.Node} Converted node.
+	 * @param {Boolean} [options.withChildren=true] It true node's and document fragment's children will be converted too.
+	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment} Converted node or document fragment.
 	 */
 	domToView( domNode, options ) {
 		if ( !options ) {
@@ -150,16 +177,29 @@ export default class DomConverter {
 				return this.getCorrespondingView( domNode );
 			}
 
-			const viewElement = new ViewElement( domNode.tagName.toLowerCase() );
+			let viewElement;
 
-			if ( options.bind ) {
-				this.bindElements( domNode, viewElement );
-			}
+			if ( domNode instanceof  DocumentFragment ) {
+				// Create view document fragment.
+				viewElement = new ViewDocumentFragment();
 
-			const attrs = domNode.attributes;
+				if ( options.bind ) {
+					this.bindDocumentFragments( domNode, viewElement );
+				}
+			} else {
+				// Create view element.
+				viewElement = new ViewElement( domNode.tagName.toLowerCase() );
 
-			for ( let i = attrs.length - 1; i >= 0; i-- ) {
-				viewElement.setAttribute( attrs[ i ].name, attrs[ i ].value );
+				if ( options.bind ) {
+					this.bindElements( domNode, viewElement );
+				}
+
+				// Copy element's attributes.
+				const attrs = domNode.attributes;
+
+				for ( let i = attrs.length - 1; i >= 0; i-- ) {
+					viewElement.setAttribute( attrs[ i ].name, attrs[ i ].value );
+				}
 			}
 
 			if ( options.withChildren || options.withChildren === undefined ) {
@@ -175,15 +215,20 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Gets corresponding view node. This function use {@link engine.treeView.DomConverter#getCorrespondingViewElement}
-	 * for elements and {@link getCorrespondingViewText} for text nodes.
+	 * Gets corresponding view item. This function use
+	 * {@link engine.treeView.DomConverter#getCorrespondingViewElement getCorrespondingViewElement}
+	 * for elements, {@link  engine.treeView.DomConverter#getCorrespondingViewText getCorrespondingViewText} for text
+	 * nodes and {@link engine.treeView.DomConverter#getCorrespondingViewDocumentFragment getCorrespondingViewDocumentFragment}
+	 * for document fragments.
 	 *
-	 * @param {Node} domNode DOM node.
-	 * @returns {engine.treeView.Node|null} Corresponding node.
+	 * @param {Node|DocumentFragment} domNode DOM node or document fragment.
+	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment|null} Corresponding view item.
 	 */
 	getCorrespondingView( domNode ) {
 		if ( domNode instanceof HTMLElement ) {
 			return this.getCorrespondingViewElement( domNode );
+		} else if ( domNode instanceof DocumentFragment ) {
+			return this.getCorrespondingViewDocumentFragment( domNode );
 		} else {
 			return this.getCorrespondingViewText( domNode );
 		}
@@ -194,12 +239,23 @@ export default class DomConverter {
 	 * {@link engine.treeView.DomConverter#bindElements bound} to the given DOM element or null otherwise.
 	 *
 	 * @param {HTMLElement} domElement DOM element.
-	 * @returns {engine.treeView.Element|null} Corresponding element or null if none element was bound.
+	 * @returns {engine.treeView.Element|null} Corresponding element or null if no element was bound.
 	 */
 	getCorrespondingViewElement( domElement ) {
 		return this._domToViewMapping.get( domElement );
 	}
 
+	/**
+	 * Gets corresponding view document fragment. Returns document fragment if an view element was
+	 * {@link engine.treeView.DomConverter#bindDocumentFragments bound} to the given DOM fragment or null otherwise.
+	 *
+	 * @param {DocumentFragment} domFragment DOM element.
+	 * @returns {engine.treeView.DocumentFragment|null} Corresponding document fragment or null if none element was bound.
+	 */
+	getCorrespondingViewDocumentFragment( domFragment ) {
+		return this._domToViewMapping.get( domFragment );
+	}
+
 	/**
 	 * Gets corresponding text node. Text nodes are not {@link engine.treeView.DomConverter#bindElements bound},
 	 * corresponding text node is returned based on the sibling or parent.
@@ -245,15 +301,20 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Gets corresponding DOM node. This function uses {@link engine.treeView.DomConverter#getCorrespondingDomElement} for
-	 * elements and {@link engine.treeView.DomConverter#getCorrespondingDomText} for text nodes.
+	 * Gets corresponding DOM item. This function uses
+	 * {@link engine.treeView.DomConverter#getCorrespondingDomElement getCorrespondingDomElement} for
+	 * elements, {@link engine.treeView.DomConverter#getCorrespondingDomText getCorrespondingDomText} for text nodes
+	 * and {@link engine.treeView.DomConverter#getCorrespondingDomDocumentFragment getCorrespondingDomDocumentFragment}
+	 * for document fragments.
 	 *
-	 * @param {engine.treeView.Node} viewNode View node.
-	 * @returns {Node|null} Corresponding DOM node.
+	 * @param {engine.treeView.Node|engine.treeView.DomFragment} viewNode View node or document fragment.
+	 * @returns {Node|DocumentFragment|null} Corresponding DOM node or document fragment.
 	 */
 	getCorrespondingDom( viewNode ) {
 		if ( viewNode instanceof ViewElement ) {
 			return this.getCorrespondingDomElement( viewNode );
+		} else if ( viewNode instanceof ViewDocumentFragment ) {
+			return this.getCorrespondingDomDocumentFragment( viewNode );
 		} else {
 			return this.getCorrespondingDomText( viewNode );
 		}
@@ -270,6 +331,17 @@ export default class DomConverter {
 		return this._viewToDomMapping.get( viewElement );
 	}
 
+	/**
+	 * Gets corresponding DOM document fragment. Returns document fragment if an DOM element was
+	 * {@link engine.treeView.DomConverter#bindDocumentFragments bound} to the given view document fragment or null otherwise.
+	 *
+	 * @param {engine.treeView.DocumentFragment} viewDocumentFragment View document fragment.
+	 * @returns {DocumentFragment|null} Corresponding document fragment or null if no fragment was bound.
+	 */
+	getCorrespondingDomDocumentFragment( viewDocumentFragment ) {
+		return this._viewToDomMapping.get( viewDocumentFragment );
+	}
+
 	/**
 	 * Gets corresponding text node. Text nodes are not {@link engine.treeView.DomConverter#bindElements bound},
 	 * corresponding text node is returned based on the sibling or parent.

+ 1 - 1
packages/ckeditor5-engine/src/treeview/node.js

@@ -26,7 +26,7 @@ export default class Node {
 		 * Parent element. Null by default. Set by {@link engine.treeView.Element#insertChildren}.
 		 *
 		 * @readonly
-		 * @member {engine.treeView.Element|null} engine.treeView.Node#parent
+		 * @member {engine.treeView.Element|engine.treeView.DocumentFragment|null} engine.treeView.Node#parent
 		 */
 		this.parent = null;
 

+ 4 - 3
packages/ckeditor5-engine/src/treeview/writer.js

@@ -10,6 +10,7 @@ import Element from './element.js';
 import Text from './text.js';
 import Range from './range.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
+import DocumentFragment from './documentfragment.js';
 
 /**
  * Tree View Writer class.
@@ -352,7 +353,7 @@ import CKEditorError from '../../utils/ckeditorerror.js';
 	 *
 	 * @param {engine.treeView.Range} range Range to remove from container. After removing, it will be updated
 	 * to a collapsed range showing the new position.
-	 * @returns {Array.<engine.treeView.Node>} The array of removed nodes.
+	 * @returns {engine.treeView.DocumentFragment} Document fragment containing removed nodes.
 	 */
 	remove( range ) {
 		// Range should be placed inside one container.
@@ -367,7 +368,7 @@ import CKEditorError from '../../utils/ckeditorerror.js';
 
 		// If range is collapsed - nothing to remove.
 		if ( range.isCollapsed ) {
-			return [];
+			return new DocumentFragment();
 		}
 
 		// Break attributes at range start and end.
@@ -385,7 +386,7 @@ import CKEditorError from '../../utils/ckeditorerror.js';
 		range.end = Position.createFromPosition( mergePosition );
 
 		// Return removed nodes.
-		return removed;
+		return new DocumentFragment( removed );
 	}
 
 	/**

+ 1 - 1
packages/ckeditor5-engine/tests/dataprocessor/htmldataprocessor.js

@@ -53,7 +53,7 @@ describe( 'HtmlDataProcessor', () => {
 		} );
 
 		// Test against XSS attacks.
-		for ( const name in xssTemplates ) {
+		for ( let name in xssTemplates ) {
 			const input = xssTemplates[ name ].replace( /%xss%/g, 'testXss()' );
 
 			it( 'should prevent XSS attacks: ' + name, ( done ) => {

+ 132 - 0
packages/ckeditor5-engine/tests/treemodel/documentfragment.js

@@ -0,0 +1,132 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treemodel */
+
+'use strict';
+
+import NodeList from '/ckeditor5/engine/treemodel/nodelist.js';
+import Element from '/ckeditor5/engine/treemodel/element.js';
+import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
+
+describe( 'DocumentFragment', () => {
+	describe( 'constructor', () => {
+		it( 'should create empty document fragment', () => {
+			let frag = new DocumentFragment();
+
+			expect( frag.getChildCount() ).to.equal( 0 );
+		} );
+
+		it( 'should create document fragment with children', () => {
+			let frag = new DocumentFragment( [ 'x', new Element( 'p' ), 'y' ] );
+
+			expect( frag.getChildCount() ).to.equal( 3 );
+			expect( frag.getChild( 0 ) ).to.have.property( 'character' ).that.equals( 'x' );
+			expect( frag.getChild( 1 ) ).to.have.property( 'name' ).that.equals( 'p' );
+			expect( frag.getChild( 2 ) ).to.have.property( 'character' ).that.equals( 'y' );
+		} );
+	} );
+
+	describe( 'insertChildren', () => {
+		it( 'should add children to the document fragment', () => {
+			let frag = new DocumentFragment( 'xy' );
+			frag.insertChildren( 1, 'foo' );
+
+			expect( frag.getChildCount() ).to.equal( 5 );
+			expect( frag.getChild( 0 ) ).to.have.property( 'character' ).that.equals( 'x' );
+			expect( frag.getChild( 1 ) ).to.have.property( 'character' ).that.equals( 'f' );
+			expect( frag.getChild( 2 ) ).to.have.property( 'character' ).that.equals( 'o' );
+			expect( frag.getChild( 3 ) ).to.have.property( 'character' ).that.equals( 'o' );
+			expect( frag.getChild( 4 ) ).to.have.property( 'character' ).that.equals( 'y' );
+		} );
+
+		it( 'should accept DocumentFragment as a parameter and clean it after it is added', () => {
+			let p1 = new Element( 'p' );
+			let p2 = new Element( 'p' );
+			let otherFrag = new DocumentFragment( [ p1, p2 ] );
+
+			let frag = new DocumentFragment();
+
+			frag.insertChildren( 0, otherFrag );
+
+			expect( frag.getChildCount() ).to.equal( 2 );
+			expect( frag.getChild( 0 ) ).to.equal( p1 );
+			expect( frag.getChild( 1 ) ).to.equal( p2 );
+			expect( otherFrag.getChildCount() ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'appendChildren', () => {
+		it( 'should add children to the end of the element', () => {
+			let frag = new DocumentFragment( 'xy' );
+			frag.appendChildren( 'foo' );
+
+			expect( frag.getChildCount() ).to.equal( 5 );
+			expect( frag.getChild( 0 ) ).to.have.property( 'character' ).that.equals( 'x' );
+			expect( frag.getChild( 1 ) ).to.have.property( 'character' ).that.equals( 'y' );
+			expect( frag.getChild( 2 ) ).to.have.property( 'character' ).that.equals( 'f' );
+			expect( frag.getChild( 3 ) ).to.have.property( 'character' ).that.equals( 'o' );
+			expect( frag.getChild( 4 ) ).to.have.property( 'character' ).that.equals( 'o' );
+		} );
+	} );
+
+	describe( 'removeChildren', () => {
+		it( 'should remove children from the element and return them as a NodeList', () => {
+			let frag = new DocumentFragment( 'foobar' );
+			let removed = frag.removeChildren( 2, 3 );
+
+			expect( frag.getChildCount() ).to.equal( 3 );
+			expect( frag.getChild( 0 ) ).to.have.property( 'character' ).that.equals( 'f' );
+			expect( frag.getChild( 1 ) ).to.have.property( 'character' ).that.equals( 'o' );
+			expect( frag.getChild( 2 ) ).to.have.property( 'character' ).that.equals( 'r' );
+
+			expect( removed ).to.be.instanceof( NodeList );
+			expect( removed.length ).to.equal( 3 );
+
+			expect( removed.get( 0 ).character ).to.equal( 'o' );
+			expect( removed.get( 1 ).character ).to.equal( 'b' );
+			expect( removed.get( 2 ).character ).to.equal( 'a' );
+		} );
+
+		it( 'should remove one child when second parameter is not specified', () => {
+			let frag = new DocumentFragment( 'foo' );
+			let removed = frag.removeChildren( 2 );
+
+			expect( frag.getChildCount() ).to.equal( 2 );
+			expect( frag.getChild( 0 ) ).to.have.property( 'character' ).that.equals( 'f' );
+			expect( frag.getChild( 1 ) ).to.have.property( 'character' ).that.equals( 'o' );
+
+			expect( removed ).to.be.instanceof( NodeList );
+			expect( removed.length ).to.equal( 1 );
+
+			expect( removed.get( 0 ).character ).to.equal( 'o' );
+		} );
+	} );
+
+	describe( 'getChildIndex', () => {
+		it( 'should return child index', () => {
+			let frag = new DocumentFragment( [ new Element( 'p' ), 'bar', new Element( 'h' ) ] );
+			let p = frag.getChild( 0 );
+			let b = frag.getChild( 1 );
+			let a = frag.getChild( 2 );
+			let r = frag.getChild( 3 );
+			let h = frag.getChild( 4 );
+
+			expect( frag.getChildIndex( p ) ).to.equal( 0 );
+			expect( frag.getChildIndex( b ) ).to.equal( 1 );
+			expect( frag.getChildIndex( a ) ).to.equal( 2 );
+			expect( frag.getChildIndex( r ) ).to.equal( 3 );
+			expect( frag.getChildIndex( h ) ).to.equal( 4 );
+		} );
+	} );
+
+	describe( 'getChildCount', () => {
+		it( 'should return number of children', () => {
+			let frag = new DocumentFragment( 'bar' );
+
+			expect( frag.getChildCount() ).to.equal( 3 );
+		} );
+	} );
+} );

+ 31 - 0
packages/ckeditor5-engine/tests/treemodel/element.js

@@ -10,6 +10,7 @@
 import Node from '/ckeditor5/engine/treemodel/node.js';
 import NodeList from '/ckeditor5/engine/treemodel/nodelist.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
+import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
 
 describe( 'Element', () => {
 	describe( 'constructor', () => {
@@ -60,6 +61,21 @@ describe( 'Element', () => {
 			expect( element.getChild( 3 ) ).to.have.property( 'character' ).that.equals( 'o' );
 			expect( element.getChild( 4 ) ).to.have.property( 'character' ).that.equals( 'y' );
 		} );
+
+		it( 'should accept DocumentFragment as a parameter and clean it after it is added', () => {
+			let p1 = new Element( 'p' );
+			let p2 = new Element( 'p' );
+			let frag = new DocumentFragment( [ p1, p2 ] );
+
+			let element = new Element( 'div' );
+
+			element.insertChildren( 0, frag );
+
+			expect( element.getChildCount() ).to.equal( 2 );
+			expect( element.getChild( 0 ) ).to.equal( p1 );
+			expect( element.getChild( 1 ) ).to.equal( p2 );
+			expect( frag.getChildCount() ).to.equal( 0 );
+		} );
 	} );
 
 	describe( 'appendChildren', () => {
@@ -75,6 +91,21 @@ describe( 'Element', () => {
 			expect( element.getChild( 3 ) ).to.have.property( 'character' ).that.equals( 'o' );
 			expect( element.getChild( 4 ) ).to.have.property( 'character' ).that.equals( 'o' );
 		} );
+
+		it( 'should accept DocumentFragment as a parameter and clean it after it is added', () => {
+			let p1 = new Element( 'p' );
+			let p2 = new Element( 'p' );
+			let frag = new DocumentFragment( [ p1, p2 ] );
+
+			let element = new Element( 'div' );
+
+			element.appendChildren( frag );
+
+			expect( element.getChildCount() ).to.equal( 2 );
+			expect( element.getChild( 0 ) ).to.equal( p1 );
+			expect( element.getChild( 1 ) ).to.equal( p2 );
+			expect( frag.getChildCount() ).to.equal( 0 );
+		} );
 	} );
 
 	describe( 'removeChildren', () => {

+ 8 - 0
packages/ckeditor5-engine/tests/treemodel/liveposition.js

@@ -8,10 +8,12 @@
 'use strict';
 
 import Document from '/ckeditor5/engine/treemodel/document.js';
+import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
 import LivePosition from '/ckeditor5/engine/treemodel/liveposition.js';
 import Range from '/ckeditor5/engine/treemodel/range.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 
 describe( 'LivePosition', () => {
 	let doc, root, ul, p, li1, li2;
@@ -35,6 +37,12 @@ describe( 'LivePosition', () => {
 		expect( live ).to.be.instanceof( Position );
 	} );
 
+	it( 'should throw if given root is not a RootElement', () => {
+		expect( () => {
+			new LivePosition( new DocumentFragment(), [ 1 ] );
+		} ).to.throw( CKEditorError, /liveposition-root-not-rootelement/ );
+	} );
+
 	it( 'should listen to a change event of the document that owns this position root', () => {
 		sinon.spy( LivePosition.prototype, 'listenTo' );
 

+ 13 - 0
packages/ckeditor5-engine/tests/treemodel/nodelist.js

@@ -8,6 +8,7 @@
 'use strict';
 
 import NodeList from '/ckeditor5/engine/treemodel/nodelist.js';
+import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import Text from '/ckeditor5/engine/treemodel/text.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
@@ -105,6 +106,18 @@ describe( 'NodeList', () => {
 			expect( nodeList._nodes[ 1 ].text ).to.equal( 'xy' );
 			expect( nodeList._nodes[ 2 ].text ).to.equal( 'bar' );
 		} );
+
+		it( 'should accept DocumentFragment as a parameter', () => {
+			let p1 = new Element( 'p' );
+			let p2 = new Element( 'p' );
+			let frag = new DocumentFragment( [ p1, p2 ] );
+
+			let nodeList = new NodeList( frag );
+
+			expect( nodeList.length ).to.equal( 2 );
+			expect( nodeList.get( 0 ) ).to.equal( p1 );
+			expect( nodeList.get( 1 ) ).to.equal( p2 );
+		} );
 	} );
 
 	describe( 'insert', () => {

+ 10 - 3
packages/ckeditor5-engine/tests/treemodel/position.js

@@ -8,6 +8,7 @@
 'use strict';
 
 import Document from '/ckeditor5/engine/treemodel/document.js';
+import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
@@ -58,6 +59,12 @@ describe( 'position', () => {
 		expect( position ).to.have.property( 'root' ).that.equals( root );
 	} );
 
+	it( 'should accept DocumentFragment as a root', () => {
+		expect( () => {
+			new Position( new DocumentFragment(), [ 0 ] );
+		} ).not.to.throw;
+	} );
+
 	it( 'should throw error if given path is incorrect', () => {
 		expect( () => {
 			new Position( root, {} );
@@ -68,14 +75,14 @@ describe( 'position', () => {
 		} ).to.throw( CKEditorError, /position-path-incorrect/ );
 	} );
 
-	it( 'should throw error if given root is not a RootElement instance', () => {
+	it( 'should throw error if given root is invalid', () => {
 		expect( () => {
 			new Position();
-		} ).to.throw( CKEditorError, /position-root-not-rootelement/ );
+		} ).to.throw( CKEditorError, /position-root-invalid/ );
 
 		expect( () => {
 			new Position( new Element( 'p' ), [ 0 ] );
-		} ).to.throw( CKEditorError, /position-root-not-rootelement/ );
+		} ).to.throw( CKEditorError, /position-root-invalid/ );
 	} );
 
 	it( 'should create positions form node and offset', () => {

+ 76 - 3
packages/ckeditor5-engine/tests/treemodel/textfragment.js

@@ -16,14 +16,12 @@ import CharacterProxy from '/ckeditor5/engine/treemodel/characterproxy.js';
 describe( 'TextFragment', () => {
 	let doc, text, element, textFragment, root;
 
-	before( () => {
+	beforeEach( () => {
 		doc = new Document();
 		root = doc.createRoot( 'root' );
 		element = new Element( 'div' );
 		root.insertChildren( 0, element );
-	} );
 
-	beforeEach( () => {
 		text = new Text( 'foobar', { foo: 'bar' } );
 		element.insertChildren( 0, text );
 		textFragment = new TextFragment( element.getChild( 2 ), 3 );
@@ -95,5 +93,80 @@ describe( 'TextFragment', () => {
 				expect( attrs ).to.deep.equal( [ [ 'foo', 'bar' ] ] );
 			} );
 		} );
+
+		describe( 'setAttribute', () => {
+			it( 'should set attribute on characters contained in text fragment', () => {
+				textFragment.setAttribute( 'abc', 'xyz' );
+
+				expect( element.getChild( 0 ).getAttribute( 'abc' ) ).to.be.undefined;
+				expect( element.getChild( 1 ).getAttribute( 'abc' ) ).to.be.undefined;
+				expect( element.getChild( 2 ).getAttribute( 'abc' ) ).to.equal( 'xyz' );
+				expect( element.getChild( 3 ).getAttribute( 'abc' ) ).to.equal( 'xyz' );
+				expect( element.getChild( 4 ).getAttribute( 'abc' ) ).to.equal( 'xyz' );
+				expect( element.getChild( 5 ).getAttribute( 'abc' ) ).to.be.undefined;
+			} );
+
+			it( 'should remove attribute when passed attribute value is null', () => {
+				textFragment.setAttribute( 'foo', null );
+
+				expect( element.getChild( 0 ).hasAttribute( 'foo' ) ).to.be.true;
+				expect( element.getChild( 1 ).hasAttribute( 'foo' ) ).to.be.true;
+				expect( element.getChild( 2 ).hasAttribute( 'foo' ) ).to.be.false;
+				expect( element.getChild( 3 ).hasAttribute( 'foo' ) ).to.be.false;
+				expect( element.getChild( 4 ).hasAttribute( 'foo' ) ).to.be.false;
+				expect( element.getChild( 5 ).hasAttribute( 'foo' ) ).to.be.true;
+			} );
+
+			it( 'should correctly split and merge text fragments and refresh this text fragment properties', () => {
+				let otherTextFragment = new TextFragment( element.getChild( 5 ), 1 );
+				otherTextFragment.setAttribute( 'foo', null );
+				textFragment.setAttribute( 'foo', null );
+
+				expect( element._children._nodes.length ).to.equal( 2 );
+				expect( textFragment.first._nodeListText ).to.equal( element._children._nodes[ 1 ] );
+			} );
+		} );
+
+		describe( 'setAttributesTo', () => {
+			it( 'should remove all attributes from text fragment and set given ones', () => {
+				textFragment.setAttributesTo( { abc: 'xyz' } );
+
+				expect( element.getChild( 2 ).hasAttribute( 'foo' ) ).to.be.false;
+				expect( element.getChild( 3 ).hasAttribute( 'foo' ) ).to.be.false;
+				expect( element.getChild( 4 ).hasAttribute( 'foo' ) ).to.be.false;
+
+				expect( element.getChild( 2 ).getAttribute( 'abc' ) ).to.equal( 'xyz' );
+				expect( element.getChild( 3 ).getAttribute( 'abc' ) ).to.equal( 'xyz' );
+				expect( element.getChild( 4 ).getAttribute( 'abc' ) ).to.equal( 'xyz' );
+			} );
+		} );
+
+		describe( 'removeAttribute', () => {
+			it( 'should remove given attribute from text fragment', () => {
+				textFragment.removeAttribute( 'foo' );
+
+				expect( element.getChild( 0 ).hasAttribute( 'foo' ) ).to.be.true;
+				expect( element.getChild( 1 ).hasAttribute( 'foo' ) ).to.be.true;
+				expect( element.getChild( 2 ).hasAttribute( 'foo' ) ).to.be.false;
+				expect( element.getChild( 3 ).hasAttribute( 'foo' ) ).to.be.false;
+				expect( element.getChild( 4 ).hasAttribute( 'foo' ) ).to.be.false;
+				expect( element.getChild( 5 ).hasAttribute( 'foo' ) ).to.be.true;
+			} );
+		} );
+
+		describe( 'clearAttributes', () => {
+			it( 'should remove all attributes from text fragment', () => {
+				textFragment.setAttribute( 'abc', 'xyz' );
+				textFragment.clearAttributes();
+
+				expect( element.getChild( 2 ).hasAttribute( 'foo' ) ).to.be.false;
+				expect( element.getChild( 3 ).hasAttribute( 'foo' ) ).to.be.false;
+				expect( element.getChild( 4 ).hasAttribute( 'foo' ) ).to.be.false;
+
+				expect( element.getChild( 2 ).hasAttribute( 'abc' ) ).to.be.false;
+				expect( element.getChild( 3 ).hasAttribute( 'abc' ) ).to.be.false;
+				expect( element.getChild( 4 ).hasAttribute( 'abc' ) ).to.be.false;
+			} );
+		} );
 	} );
 } );

+ 199 - 0
packages/ckeditor5-engine/tests/treeview/documentfragment.js

@@ -0,0 +1,199 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treeview */
+
+'use strict';
+
+import DocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
+import Element from '/ckeditor5/engine/treeview/element.js';
+import Node from '/ckeditor5/engine/treeview/node.js';
+
+describe( 'DocumentFragment', () => {
+	describe( 'constructor', () => {
+		it( 'should create DocumentFragment without children', () => {
+			const fragment = new DocumentFragment();
+
+			expect( fragment ).to.be.an.instanceof( DocumentFragment );
+			expect( fragment.getChildCount() ).to.equal( 0 );
+		} );
+
+		it( 'should create DocumentFragment with child node', () => {
+			const child = new Element( 'p' );
+			const fragment = new DocumentFragment( child );
+
+			expect( fragment.getChildCount() ).to.equal( 1 );
+			expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'p' );
+		} );
+
+		it( 'should create DocumentFragment with multiple nodes', () => {
+			const children = [ new Element( 'p' ), new Element( 'div' ) ];
+			const fragment = new DocumentFragment( children );
+
+			expect( fragment.getChildCount() ).to.equal( 2 );
+			expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'p' );
+			expect( fragment.getChild( 1 ) ).to.have.property( 'name' ).that.equals( 'div' );
+		} );
+	} );
+
+	describe( 'children manipulation methods', () => {
+		let fragment, el1, el2, el3, el4;
+
+		beforeEach( () => {
+			fragment = new DocumentFragment();
+			el1 = new Element( 'el1' );
+			el2 = new Element( 'el2' );
+			el3 = new Element( 'el3' );
+			el4 = new Element( 'el4' );
+		} );
+
+		describe( 'insertion', () => {
+			it( 'should insert children', () => {
+				const count1 = fragment.insertChildren( 0, [ el1, el3 ] );
+				const count2 = fragment.insertChildren( 1, el2 );
+
+				expect( fragment.getChildCount() ).to.equal( 3 );
+				expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'el1' );
+				expect( fragment.getChild( 1 ) ).to.have.property( 'name' ).that.equals( 'el2' );
+				expect( fragment.getChild( 2 ) ).to.have.property( 'name' ).that.equals( 'el3' );
+				expect( count1 ).to.equal( 2 );
+				expect( count2 ).to.equal( 1 );
+			} );
+
+			it( 'should append children', () => {
+				const count1 = fragment.insertChildren( 0, el1 );
+				const count2 = fragment.appendChildren( el2 );
+				const count3 = fragment.appendChildren( el3 );
+
+				expect( fragment.getChildCount() ).to.equal( 3 );
+				expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'el1' );
+				expect( fragment.getChild( 1 ) ).to.have.property( 'name' ).that.equals( 'el2' );
+				expect( fragment.getChild( 2 ) ).to.have.property( 'name' ).that.equals( 'el3' );
+				expect( count1 ).to.equal( 1 );
+				expect( count2 ).to.equal( 1 );
+				expect( count3 ).to.equal( 1 );
+			} );
+		} );
+
+		describe( 'getChildIndex', () => {
+			it( 'should return child index', () => {
+				fragment.appendChildren( el1 );
+				fragment.appendChildren( el2 );
+				fragment.appendChildren( el3 );
+
+				expect( fragment.getChildCount() ).to.equal( 3 );
+				expect( fragment.getChildIndex( el1 ) ).to.equal( 0 );
+				expect( fragment.getChildIndex( el2 ) ).to.equal( 1 );
+				expect( fragment.getChildIndex( el3 ) ).to.equal( 2 );
+			} );
+		} );
+
+		describe( 'getChildren', () => {
+			it( 'should renturn children iterator', () => {
+				fragment.appendChildren( el1 );
+				fragment.appendChildren( el2 );
+				fragment.appendChildren( el3 );
+
+				const expected = [ el1, el2, el3 ];
+				let i = 0;
+
+				for ( let child of fragment.getChildren() ) {
+					expect( child ).to.equal( expected[ i ] );
+					i++;
+				}
+
+				expect( i ).to.equal( 3 );
+			} );
+		} );
+
+		describe( 'removeChildren', () => {
+			it( 'should remove children', () => {
+				fragment.appendChildren( el1 );
+				fragment.appendChildren( el2 );
+				fragment.appendChildren( el3 );
+				fragment.appendChildren( el4 );
+
+				fragment.removeChildren( 1, 2 );
+
+				expect( fragment.getChildCount() ).to.equal( 2 );
+				expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'el1' );
+				expect( fragment.getChild( 1 ) ).to.have.property( 'name' ).that.equals( 'el4' );
+
+				expect( el1.parent ).to.equal( fragment );
+				expect( el2.parent ).to.be.null;
+				expect( el3.parent ).to.be.null;
+				expect( el4.parent ).equal( fragment );
+			} );
+
+			it( 'should remove one child when second parameter is not specified', () => {
+				fragment.appendChildren( el1 );
+				fragment.appendChildren( el2 );
+				fragment.appendChildren( el3 );
+
+				const removed = fragment.removeChildren( 1 );
+
+				expect( fragment.getChildCount() ).to.equal( 2 );
+				expect( fragment.getChild( 0 ) ).to.have.property( 'name' ).that.equals( 'el1' );
+				expect( fragment.getChild( 1 ) ).to.have.property( 'name' ).that.equals( 'el3' );
+
+				expect( removed.length ).to.equal( 1 );
+				expect( removed[ 0 ] ).to.have.property( 'name' ).that.equals( 'el2' );
+			} );
+		} );
+	} );
+
+	describe( 'node methods when inserted to fragment', () => {
+		it( 'getIndex() should return proper value', () => {
+			const node1 = new Node();
+			const node2 = new Node();
+			const node3 = new Node();
+			const fragment = new DocumentFragment( [ node1, node2, node3 ] );
+
+			expect( node1.getIndex() ).to.equal( 0 );
+			expect( node2.getIndex() ).to.equal( 1 );
+			expect( node3.getIndex() ).to.equal( 2 );
+			expect( node1.parent ).to.equal( fragment );
+			expect( node2.parent ).to.equal( fragment );
+			expect( node3.parent ).to.equal( fragment );
+		} );
+
+		it( 'getNextSibling() should return proper node', () => {
+			const node1 = new Node();
+			const node2 = new Node();
+			const node3 = new Node();
+			new DocumentFragment( [ node1, node2, node3 ] );
+
+			expect( node1.getNextSibling() ).to.equal( node2 );
+			expect( node2.getNextSibling() ).to.equal( node3 );
+			expect( node3.getNextSibling() ).to.be.null;
+		} );
+
+		it( 'getPreviousSibling() should return proper node', () => {
+			const node1 = new Node();
+			const node2 = new Node();
+			const node3 = new Node();
+			new DocumentFragment( [ node1, node2, node3 ] );
+
+			expect( node1.getPreviousSibling() ).to.be.null;
+			expect( node2.getPreviousSibling() ).to.equal( node1 );
+			expect( node3.getPreviousSibling() ).to.equal( node2 );
+		} );
+
+		it( 'remove() should remove node from fragment', () => {
+			const node1 = new Node();
+			const node2 = new Node();
+			const node3 = new Node();
+			const fragment = new DocumentFragment( [ node1, node2, node3 ] );
+
+			node1.remove();
+			node3.remove();
+
+			expect( fragment.getChildCount() ).to.equal( 1 );
+			expect( node1.parent ).to.be.null;
+			expect( node3.parent ).to.be.null;
+			expect( fragment.getChild( 0 ) ).to.equal( node2 );
+		} );
+	} );
+} );

+ 153 - 0
packages/ckeditor5-engine/tests/treeview/domconverter.js

@@ -11,6 +11,7 @@ import utils from '/ckeditor5/utils/utils.js';
 import ViewElement from '/ckeditor5/engine/treeview/element.js';
 import ViewText from '/ckeditor5/engine/treeview/text.js';
 import DomConverter from '/ckeditor5/engine/treeview/domconverter.js';
+import ViewDocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
 
 describe( 'DomConverter', () => {
 	let converter;
@@ -31,6 +32,18 @@ describe( 'DomConverter', () => {
 		} );
 	} );
 
+	describe( 'bindDocumentFragments', () => {
+		it( 'should bind document fragments', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindDocumentFragments( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
+		} );
+	} );
+
 	describe( 'compareNodes', () => {
 		it( 'should return false for nodes not matching types', () => {
 			const domElement = document.createElement( 'p' );
@@ -138,6 +151,56 @@ describe( 'DomConverter', () => {
 			expect( viewP.getChildCount() ).to.equal( 0 );
 			expect( converter.getCorrespondingDom( viewP ) ).to.not.equal( domP );
 		} );
+
+		it( 'should create view document fragment from DOM document fragment', () => {
+			const domImg = document.createElement( 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domFragment = document.createDocumentFragment();
+
+			domFragment.appendChild( domImg );
+			domFragment.appendChild( domText );
+
+			const viewFragment = converter.domToView( domFragment, { bind: true } );
+
+			expect( viewFragment ).to.be.an.instanceof( ViewDocumentFragment );
+			expect( viewFragment.getChildCount() ).to.equal( 2 );
+			expect( viewFragment.getChild( 0 ).name ).to.equal( 'img' );
+			expect( viewFragment.getChild( 1 ).data ).to.equal( 'foo' );
+
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
+			expect( converter.getCorrespondingDom( viewFragment.getChild( 0 ) ) ).to.equal( domFragment.childNodes[ 0 ] );
+		} );
+
+		it( 'should create view document fragment from DOM document fragment without children', () => {
+			const domImg = document.createElement( 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domFragment = document.createDocumentFragment();
+
+			domFragment.appendChild( domImg );
+			domFragment.appendChild( domText );
+
+			const viewImg = new ViewElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const viewFragment = converter.domToView( domFragment, { withChildren: false } );
+
+			expect( viewFragment ).to.be.an.instanceof( ViewDocumentFragment );
+
+			expect( viewFragment.getChildCount() ).to.equal( 0 );
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.not.equal( domFragment );
+		} );
+
+		it( 'should return already bind document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindDocumentFragments( domFragment, viewFragment );
+
+			const viewFragment2 = converter.domToView( domFragment );
+
+			expect( viewFragment2 ).to.equal( viewFragment );
+		} );
 	} );
 
 	describe( 'viewToDom', () => {
@@ -222,6 +285,56 @@ describe( 'DomConverter', () => {
 			expect( domP.childNodes.length ).to.equal( 0 );
 			expect( converter.getCorrespondingView( domP ) ).not.to.equal( viewP );
 		} );
+
+		it( 'should create DOM document fragment from view document fragment and bind elements', () => {
+			const viewImg = new ViewElement( 'img' );
+			const viewText = new ViewText( 'foo' );
+			const viewFragment = new ViewDocumentFragment();
+
+			viewFragment.appendChildren( viewImg );
+			viewFragment.appendChildren( viewText );
+
+			const domFragment = converter.viewToDom( viewFragment, document, { bind: true } );
+
+			expect( domFragment ).to.be.an.instanceof( DocumentFragment );
+			expect( domFragment.childNodes.length ).to.equal( 2 );
+			expect( domFragment.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
+			expect( domFragment.childNodes[ 1 ].data ).to.equal( 'foo' );
+
+			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
+			expect( converter.getCorrespondingView( domFragment.childNodes[ 0 ] ) ).to.equal( viewFragment.getChild( 0 ) );
+		} );
+
+		it( 'should create DOM document fragment from view document without children', () => {
+			const viewImg = new ViewElement( 'img' );
+			const viewText = new ViewText( 'foo' );
+			const viewFragment = new ViewDocumentFragment();
+
+			viewFragment.appendChildren( viewImg );
+			viewFragment.appendChildren( viewText );
+
+			const domImg = document.createElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const domFragment = converter.viewToDom( viewFragment, document, { withChildren: false } );
+
+			expect( domFragment ).to.be.an.instanceof( DocumentFragment );
+
+			expect( domFragment.childNodes.length ).to.equal( 0 );
+			expect( converter.getCorrespondingView( domFragment ) ).not.to.equal( viewFragment );
+		} );
+
+		it( 'should return already bind document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindDocumentFragments( domFragment, viewFragment );
+
+			const domFragment2 = converter.viewToDom( viewFragment );
+
+			expect( domFragment2 ).to.equal( domFragment );
+		} );
 	} );
 
 	describe( 'getCorrespondingView', () => {
@@ -247,6 +360,15 @@ describe( 'DomConverter', () => {
 
 			expect( converter.getCorrespondingView( domText ) ).to.equal( viewText );
 		} );
+
+		it( 'should return corresponding view document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = converter.domToView( domFragment );
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
+		} );
 	} );
 
 	describe( 'getCorrespondingViewElement', () => {
@@ -260,6 +382,17 @@ describe( 'DomConverter', () => {
 		} );
 	} );
 
+	describe( 'getCorrespondingViewDocumentFragment', () => {
+		it( 'should return corresponding view document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = converter.domToView( domFragment );
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingViewDocumentFragment( domFragment ) ).to.equal( viewFragment );
+		} );
+	} );
+
 	describe( 'getCorrespondingViewText', () => {
 		it( 'should return corresponding view text based on sibling', () => {
 			const domImg = document.createElement( 'img' );
@@ -356,6 +489,15 @@ describe( 'DomConverter', () => {
 
 			expect( converter.getCorrespondingDom( viewText ) ).to.equal( domText );
 		} );
+
+		it( 'should return corresponding DOM document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
+		} );
 	} );
 
 	describe( 'getCorrespondingDomElement', () => {
@@ -369,6 +511,17 @@ describe( 'DomConverter', () => {
 		} );
 	} );
 
+	describe( 'getCorrespondingDomDocumentFragment', () => {
+		it( 'should return corresponding DOM document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingDomDocumentFragment( viewFragment ) ).to.equal( domFragment );
+		} );
+	} );
+
 	describe( 'getCorrespondingDomText', () => {
 		it( 'should return corresponding DOM text based on sibling', () => {
 			const domImg = document.createElement( 'img' );

+ 6 - 5
packages/ckeditor5-engine/tests/treeview/writer/remove.js

@@ -11,6 +11,7 @@ import Writer from '/ckeditor5/engine/treeview/writer.js';
 import Element from '/ckeditor5/engine/treeview/element.js';
 import Range from '/ckeditor5/engine/treeview/range.js';
 import Text from '/ckeditor5/engine/treeview/text.js';
+import DocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
 import utils from '/tests/engine/treeview/writer/_utils/utils.js';
 
 describe( 'Writer', () => {
@@ -32,13 +33,13 @@ describe( 'Writer', () => {
 			} ).to.throw( 'treeview-writer-invalid-range-container' );
 		} );
 
-		it( 'should return empty array when range is collapsed', () => {
+		it( 'should return empty DocumentFragment when range is collapsed', () => {
 			const p = new Element( 'p' );
 			const range = Range.createFromParentsAndOffsets( p, 0, p, 0 );
-			const nodes = writer.remove( range );
+			const fragment = writer.remove( range );
 
-			expect( nodes ).to.be.array;
-			expect( nodes.length ).to.equal( 0 );
+			expect( fragment ).to.be.instanceof( DocumentFragment );
+			expect( fragment.getChildCount() ).to.equal( 0 );
 			expect( range.isCollapsed ).to.be.true;
 		} );
 
@@ -63,7 +64,7 @@ describe( 'Writer', () => {
 			} );
 
 			// Test removed nodes.
-			test( writer, null, removed, [
+			test( writer, null, Array.from( removed.getChildren() ), [
 				{ instanceOf: Text, data: 'foobar' }
 			] );
 		} );