8
0
فهرست منبع

Merge branch 'master' into t/712

Piotrek Koszuliński 9 سال پیش
والد
کامیت
644c1ba95d

+ 33 - 13
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -138,34 +138,38 @@ export default class DataController {
 	}
 
 	/**
-	 * Returns the content of the given {@link module:engine/model/element~Element model's element} converted by the
+	 * Returns the content of the given {@link module:engine/model/element~Element model's element} or
+	 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the
 	 * {@link #modelToView model to view converters} and formatted by the
 	 * {@link #processor data processor}.
 	 *
-	 * @param {module:engine/model/element~Element} modelElement Element which content will be stringified.
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
+	 * Element which content will be stringified.
 	 * @returns {String} Output data.
 	 */
-	stringify( modelElement ) {
+	stringify( modelElementOrFragment ) {
 		// model -> view
-		const viewDocumentFragment = this.toView( modelElement );
+		const viewDocumentFragment = this.toView( modelElementOrFragment );
 
 		// view -> data
 		return this.processor.toData( viewDocumentFragment );
 	}
 
 	/**
-	 * Returns the content of the given {@link module:engine/model/element~Element model's element} converted by the
-	 * {@link #modelToView model to view converters} to the
-	 * {@link module:engine/view/documentfragment~DocumentFragment view DocumentFragment}.
+	 * Returns the content of the given {@link module:engine/model/element~Element model element} or
+	 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the
+	 * {@link #modelToView model to view converters} to a
+	 * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}.
 	 *
-	 * @param {module:engine/model/element~Element} modelElement Element which content will be stringified.
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
+	 * Element or document fragment which content will be converted.
 	 * @returns {module:engine/view/documentfragment~DocumentFragment} Output view DocumentFragment.
 	 */
-	toView( modelElement ) {
-		const modelRange = ModelRange.createIn( modelElement );
+	toView( modelElementOrFragment ) {
+		const modelRange = ModelRange.createIn( modelElementOrFragment );
 
 		const viewDocumentFragment = new ViewDocumentFragment();
-		this.mapper.bindElements( modelElement, viewDocumentFragment );
+		this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
 
 		this.modelToView.convertInsertion( modelRange );
 
@@ -207,7 +211,7 @@ export default class DataController {
 	 *
 	 * @see #set
 	 * @param {String} data Data to parse.
-	 * @param {String} [context='$root'] Base context in which view will be converted to the model. See:
+	 * @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
 	 * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Parsed data.
 	 */
@@ -216,7 +220,23 @@ export default class DataController {
 		const viewDocumentFragment = this.processor.toView( data );
 
 		// view -> model
-		return this.viewToModel.convert( viewDocumentFragment, { context: [ context ] } );
+		return this.toModel( viewDocumentFragment, context );
+	}
+
+	/**
+	 * Returns the content of the given {@link module:engine/view/element~Element view element} or
+	 * {@link module:engine/view/documentfragment~DocumentFragment view document fragment} converted by the
+	 * {@link #viewToModel view to model converters} to a
+	 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
+	 *
+	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
+	 * Element or document fragment which content will be converted.
+	 * @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
+	 * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
+	 * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
+	 */
+	toModel( viewElementOrFragment, context = '$root' ) {
+		return this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } );
 	}
 
 	/**

+ 423 - 424
packages/ckeditor5-engine/src/conversion/viewconsumable.js

@@ -14,578 +14,577 @@ import ViewText from '../view/text.js';
 import ViewDocumentFragment from '../view/documentfragment.js';
 
 /**
- * This is a private helper-class for {@link module:engine/conversion/viewconsumable~ViewConsumable}.
- * It represents and manipulates consumable parts of a single {@link module:engine/view/element~Element}.
+ * Class used for handling consumption of view {@link module:engine/view/element~Element elements},
+ * {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}.
+ * Element's name and its parts (attributes, classes and styles) can be consumed separately. Consuming an element's name
+ * does not consume its attributes, classes and styles.
+ * To add items for consumption use {@link module:engine/conversion/viewconsumable~ViewConsumable#add add method}.
+ * To test items use {@link module:engine/conversion/viewconsumable~ViewConsumable#test test method}.
+ * To consume items use {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume method}.
+ * To revert already consumed items use {@link module:engine/conversion/viewconsumable~ViewConsumable#revert revert method}.
  *
- * @private
+ *		viewConsumable.add( element, { name: true } ); // Adds element's name as ready to be consumed.
+ *		viewConsumable.add( textNode ); // Adds text node for consumption.
+ *		viewConsumable.add( docFragment ); // Adds document fragment for consumption.
+ *		viewConsumable.test( element, { name: true }  ); // Tests if element's name can be consumed.
+ *		viewConsumable.test( textNode ); // Tests if text node can be consumed.
+ *		viewConsumable.test( docFragment ); // Tests if document fragment can be consumed.
+ *		viewConsumable.consume( element, { name: true }  ); // Consume element's name.
+ *		viewConsumable.consume( textNode ); // Consume text node.
+ *		viewConsumable.consume( docFragment ); // Consume document fragment.
+ *		viewConsumable.revert( element, { name: true }  ); // Revert already consumed element's name.
+ *		viewConsumable.revert( textNode ); // Revert already consumed text node.
+ *		viewConsumable.revert( docFragment ); // Revert already consumed document fragment.
  */
-class ViewElementConsumables {
-
+export default class ViewConsumable {
 	/**
-	 * Creates ViewElementConsumables instance.
+	 * Creates new ViewConsumable.
 	 */
-	constructor()  {
-		/**
-		 * Flag indicating if name of the element can be consumed.
-		 *
-		 * @private
-		 * @member {Boolean}
-		 */
-		this._canConsumeName = null;
-
+	constructor() {
 		/**
-		 * Contains maps of element's consumables: attributes, classes and styles.
+		 * Map of consumable elements. If {@link module:engine/view/element~Element element} is used as a key,
+		 * {@link module:engine/conversion/viewconsumable~ViewElementConsumables ViewElementConsumables} instance is stored as value.
+		 * For {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}
+		 * boolean value is stored as value.
 		 *
-		 * @private
-		 * @member {Object}
-		 */
-		this._consumables = {
-			attribute: new Map(),
-			style: new Map(),
-			class: new Map()
-		};
+		 * @protected
+		 * @member {Map.<module:engine/conversion/viewconsumable~ViewElementConsumables|Boolean>}
+		*/
+		this._consumables = new Map();
 	}
 
 	/**
-	 * Adds consumable parts of the {@link module:engine/view/element~Element view element}.
-	 * Element's name itself can be marked to be consumed (when element's name is consumed its attributes, classes and
-	 * styles still could be consumed):
-	 *
-	 *		consumables.add( { name: true } );
-	 *
-	 * Attributes classes and styles:
+	 * Adds {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
+	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} as ready to be consumed.
 	 *
-	 *		consumables.add( { attribute: 'title', class: 'foo', style: 'color' } );
-	 *		consumables.add( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
+	 *		viewConsumable.add( p, { name: true } ); // Adds element's name to consume.
+	 *		viewConsumable.add( p, { attribute: 'name' } ); // Adds element's attribute.
+	 *		viewConsumable.add( p, { class: 'foobar' } ); // Adds element's class.
+	 *		viewConsumable.add( p, { style: 'color' } ); // Adds element's style
+	 *		viewConsumable.add( p, { attribute: 'name', style: 'color' } ); // Adds attribute and style.
+	 *		viewConsumable.add( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be provided.
+	 *		viewConsumable.add( textNode ); // Adds text node to consume.
+	 *		viewConsumable.add( docFragment ); // Adds document fragment to consume.
 	 *
 	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
-	 * attribute is provided - it should be handled separately by providing `style` and `class` in consumables object.
+	 * attribute is provided - it should be handled separately by providing actual style/class.
 	 *
-	 * @param {Object} consumables Object describing which parts of the element can be consumed.
-	 * @param {Boolean} consumables.name If set to `true` element's name will be added as consumable.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to add as consumable.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names to add as consumable.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names to add as consumable.
+	 *		viewConsumable.add( p, { attribute: 'style' } ); // This call will throw an exception.
+	 *		viewConsumable.add( p, { style: 'color' } ); // This is properly handled style.
+	 *
+	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
 	 */
-	add( consumables ) {
-		if ( consumables.name ) {
-			this._canConsumeName = true;
-		}
+	add( element, consumables ) {
+		let elementConsumables;
 
-		for ( let type in this._consumables ) {
-			if ( type in consumables ) {
-				this._add( type, consumables[ type ] );
-			}
-		}
-	}
+		// For text nodes and document fragments just mark them as consumable.
+		if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+			this._consumables.set( element, true );
 
-	/**
-	 * Tests if parts of the {@link module:engine/view/element~Element view element} can be consumed.
-	 *
-	 * Element's name can be tested:
-	 *
-	 *		consumables.test( { name: true } );
-	 *
-	 * Attributes classes and styles:
-	 *
-	 *		consumables.test( { attribute: 'title', class: 'foo', style: 'color' } );
-	 *		consumables.test( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
-	 *
-	 * @param {Object} consumables Object describing which parts of the element should be tested.
-	 * @param {Boolean} consumables.name If set to `true` element's name will be tested.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to test.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names to test.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names to test.
-	 * @returns {Boolean|null} `true` when all tested items can be consumed, `null` when even one of the items
-	 * was never marked for consumption and `false` when even one of the items was already consumed.
-	 */
-	test( consumables ) {
-		// Check if name can be consumed.
-		if ( consumables.name && !this._canConsumeName ) {
-			return this._canConsumeName;
+			return;
 		}
 
-		for ( let type in this._consumables ) {
-			if ( type in consumables ) {
-				const value = this._test( type, consumables[ type ] );
-
-				if ( value !== true ) {
-					return value;
-				}
-			}
+		// For elements create new ViewElementConsumables or update already existing one.
+		if ( !this._consumables.has( element ) ) {
+			elementConsumables = new ViewElementConsumables();
+			this._consumables.set( element, elementConsumables );
+		} else {
+			elementConsumables = this._consumables.get( element );
 		}
 
-		// Return true only if all can be consumed.
-		return true;
+		elementConsumables.add( consumables );
 	}
 
 	/**
-	 * Consumes parts of {@link module:engine/view/element~Element view element}. This function does not check if consumable item
-	 * is already consumed - it consumes all consumable items provided.
-	 * Element's name can be consumed:
+	 * Tests if {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
+	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} can be consumed.
+	 * It returns `true` when all items included in method's call can be consumed. Returns `false` when
+	 * first already consumed item is found and `null` when first non-consumable item is found.
 	 *
-	 *		consumables.consume( { name: true } );
+	 *		viewConsumable.test( p, { name: true } ); // Tests element's name.
+	 *		viewConsumable.test( p, { attribute: 'name' } ); // Tests attribute.
+	 *		viewConsumable.test( p, { class: 'foobar' } ); // Tests class.
+	 *		viewConsumable.test( p, { style: 'color' } ); // Tests style.
+	 *		viewConsumable.test( p, { attribute: 'name', style: 'color' } ); // Tests attribute and style.
+	 *		viewConsumable.test( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be tested.
+	 *		viewConsumable.test( textNode ); // Tests text node.
+	 *		viewConsumable.test( docFragment ); // Tests document fragment.
 	 *
-	 * Attributes classes and styles:
+	 * Testing classes and styles as attribute will test if all added classes/styles can be consumed.
 	 *
-	 *		consumables.consume( { attribute: 'title', class: 'foo', style: 'color' } );
-	 *		consumables.consume( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
+	 *		viewConsumable.test( p, { attribute: 'class' } ); // Tests if all added classes can be consumed.
+	 *		viewConsumable.test( p, { attribute: 'style' } ); // Tests if all added styles can be consumed.
 	 *
-	 * @param {Object} consumables Object describing which parts of the element should be consumed.
-	 * @param {Boolean} consumables.name If set to `true` element's name will be consumed.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to consume.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names to consume.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names to consume.
+	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
+	 * @returns {Boolean|null} Returns `true` when all items included in method's call can be consumed. Returns `false`
+	 * when first already consumed item is found and `null` when first non-consumable item is found.
 	 */
-	consume( consumables ) {
-		if ( consumables.name ) {
-			this._canConsumeName = false;
+	test( element, consumables ) {
+		const elementConsumables = this._consumables.get( element );
+
+		if ( elementConsumables === undefined ) {
+			return null;
 		}
 
-		for ( let type in this._consumables ) {
-			if ( type in consumables ) {
-				this._consume( type, consumables[ type ] );
-			}
+		// For text nodes and document fragments return stored boolean value.
+		if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+			return elementConsumables;
 		}
+
+		// For elements test consumables object.
+		return elementConsumables.test( consumables );
 	}
 
 	/**
-	 * Revert already consumed parts of {@link module:engine/view/element~Element view Element}, so they can be consumed once again.
-	 * Element's name can be reverted:
+	 * Consumes {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
+	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
+	 * It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
 	 *
-	 *		consumables.revert( { name: true } );
+	 *		viewConsumable.consume( p, { name: true } ); // Consumes element's name.
+	 *		viewConsumable.consume( p, { attribute: 'name' } ); // Consumes element's attribute.
+	 *		viewConsumable.consume( p, { class: 'foobar' } ); // Consumes element's class.
+	 *		viewConsumable.consume( p, { style: 'color' } ); // Consumes element's style.
+	 *		viewConsumable.consume( p, { attribute: 'name', style: 'color' } ); // Consumes attribute and style.
+	 *		viewConsumable.consume( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be consumed.
+	 *		viewConsumable.consume( textNode ); // Consumes text node.
+	 *		viewConsumable.consume( docFragment ); // Consumes document fragment.
 	 *
-	 * Attributes classes and styles:
+	 * Consuming classes and styles as attribute will test if all added classes/styles can be consumed.
 	 *
-	 *		consumables.revert( { attribute: 'title', class: 'foo', style: 'color' } );
-	 *		consumables.revert( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
+	 *		viewConsumable.consume( p, { attribute: 'class' } ); // Consume only if all added classes can be consumed.
+	 *		viewConsumable.consume( p, { attribute: 'style' } ); // Consume only if all added styles can be consumed.
 	 *
-	 * @param {Object} consumables Object describing which parts of the element should be reverted.
-	 * @param {Boolean} consumables.name If set to `true` element's name will be reverted.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to revert.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names to revert.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names to revert.
+	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
+	 * @returns {Boolean} Returns `true` when all items included in method's call can be consumed,
+	 * otherwise returns `false`.
 	 */
-	revert( consumables ) {
-		if ( consumables.name ) {
-			this._canConsumeName = true;
-		}
-
-		for ( let type in this._consumables ) {
-			if ( type in consumables ) {
-				this._revert( type, consumables[ type ] );
+	consume( element, consumables ) {
+		if ( this.test( element, consumables ) ) {
+			if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+				// For text nodes and document fragments set value to false.
+				this._consumables.set( element, false );
+			} else {
+				// For elements - consume consumables object.
+				this._consumables.get( element ).consume( consumables );
 			}
+
+			return true;
 		}
+
+		return false;
 	}
 
 	/**
-	 * Helper method that adds consumables of a given type: attribute, class or style.
+	 * Reverts {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
+	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} so they can be consumed once again.
+	 * Method does not revert items that were never previously added for consumption, even if they are included in
+	 * method's call.
 	 *
-	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
-	 * type is provided - it should be handled separately by providing actual style/class type.
+	 *		viewConsumable.revert( p, { name: true } ); // Reverts element's name.
+	 *		viewConsumable.revert( p, { attribute: 'name' } ); // Reverts element's attribute.
+	 *		viewConsumable.revert( p, { class: 'foobar' } ); // Reverts element's class.
+	 *		viewConsumable.revert( p, { style: 'color' } ); // Reverts element's style.
+	 *		viewConsumable.revert( p, { attribute: 'name', style: 'color' } ); // Reverts attribute and style.
+	 *		viewConsumable.revert( p, { class: [ 'baz', 'bar' ] } ); // Multiple names can be reverted.
+	 *		viewConsumable.revert( textNode ); // Reverts text node.
+	 *		viewConsumable.revert( docFragment ); // Reverts document fragment.
 	 *
-	 * @private
-	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
-	 * @param {String|Array.<String>} item Consumable item or array of items.
-	 */
-	_add( type, item ) {
-		const items = isArray( item ) ? item : [ item ];
-		const consumables = this._consumables[ type ];
-
-		for ( let name of items ) {
-			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
-				/**
-				 * Class and style attributes should be handled separately.
-				 *
-				 * @error viewconsumable-invalid-attribute
-				 */
-				throw new CKEditorError( 'viewconsumable-invalid-attribute: Classes and styles should be handled separately.' );
-			}
-
-			consumables.set( name, true );
-		}
-	}
-
-	/**
-	 * Helper method that tests consumables of a given type: attribute, class or style.
+	 * Reverting classes and styles as attribute will revert all classes/styles that were previously added for
+	 * consumption.
 	 *
-	 * @private
-	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
-	 * @param {String|Array.<String>} item Consumable item or array of items.
-	 * @returns {Boolean|null} Returns `true` if all items can be consumed, `null` when one of the items cannot be
-	 * consumed and `false` when one of the items is already consumed.
+	 *		viewConsumable.revert( p, { attribute: 'class' } ); // Reverts all classes added for consumption.
+	 *		viewConsumable.revert( p, { attribute: 'style' } ); // Reverts all styles added for consumption.
+	 *
+	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
 	 */
-	_test( type, item ) {
-		const items = isArray( item ) ? item : [ item ];
-		const consumables = this._consumables[ type ];
-
-		for ( let name of items ) {
-			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) )  {
-				// Check all classes/styles if class/style attribute is tested.
-				const value = this._test( name, [ ...this._consumables[ name ].keys() ] );
+	revert( element, consumables ) {
+		const elementConsumables = this._consumables.get( element );
 
-				if ( value !== true ) {
-					return value;
-				}
+		if ( elementConsumables !== undefined ) {
+			if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
+				// For text nodes and document fragments - set consumable to true.
+				this._consumables.set( element, true );
 			} else {
-				const value = consumables.get( name );
-				// Return null if attribute is not found.
-				if ( value === undefined ) {
-					return null;
-				}
-
-				if ( !value ) {
-					return false;
-				}
+				// For elements - revert items from consumables object.
+				elementConsumables.revert( consumables );
 			}
 		}
-
-		return true;
 	}
 
 	/**
-	 * Helper method that consumes items of a given type: attribute, class or style.
+	 * Creates consumable object from {@link module:engine/view/element~Element view element}. Consumable object will include
+	 * element's name and all its attributes, classes and styles.
 	 *
-	 * @private
-	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
-	 * @param {String|Array.<String>} item Consumable item or array of items.
+	 * @static
+	 * @param {module:engine/view/element~Element} element
+	 * @returns {Object} consumables
 	 */
-	_consume( type, item ) {
-		const items = isArray( item ) ? item : [ item ];
-		const consumables = this._consumables[ type ];
+	static consumablesFromElement( element ) {
+		const consumables = {
+			name: true,
+			attribute: [],
+			class: [],
+			style: []
+		};
 
-		for ( let name of items ) {
-			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
-				// If class or style is provided for consumption - consume them all.
-				this._consume( name, [ ...this._consumables[ name ].keys() ] );
-			} else {
-				consumables.set( name, false );
+		const attributes = element.getAttributeKeys();
+
+		for ( let attribute of attributes ) {
+			// Skip classes and styles - will be added separately.
+			if ( attribute == 'style' || attribute == 'class' ) {
+				continue;
 			}
+
+			consumables.attribute.push( attribute );
+		}
+
+		const classes = element.getClassNames();
+
+		for ( let className of classes ) {
+			consumables.class.push( className );
+		}
+
+		const styles = element.getStyleNames();
+
+		for ( let style of styles ) {
+			consumables.style.push( style );
 		}
+
+		return consumables;
 	}
 
 	/**
-	 * Helper method that reverts items of a given type: attribute, class or style.
+	 * Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from
+	 * {@link module:engine/view/element~Element element} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
+	 * Instance will contain all elements, child nodes, attributes, styles and classes added for consumption.
 	 *
-	 * @private
-	 * @param {String} type Type of the consumable item: `attribute`, `class` or , `style`.
-	 * @param {String|Array.<String>} item Consumable item or array of items.
+	 * @static
+	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} from View element or document fragment
+	 * from which `ViewConsumable` will be created.
+	 * @param {module:engine/conversion/viewconsumable~ViewConsumable} [instance] If provided, given `ViewConsumable` instance will be used
+	 * to add all consumables. It will be returned instead of a new instance.
 	 */
-	_revert( type, item ) {
-		const items = isArray( item ) ? item : [ item ];
-		const consumables = this._consumables[ type ];
+	static createFrom( from, instance ) {
+		if ( !instance ) {
+			instance = new ViewConsumable();
+		}
 
-		for ( let name of items ) {
-			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
-				// If class or style is provided for reverting - revert them all.
-				this._revert( name, [ ...this._consumables[ name ].keys() ] );
-			} else {
-				const value = consumables.get( name );
+		if ( from instanceof ViewText ) {
+			instance.add( from );
 
-				if ( value === false ) {
-					consumables.set( name, true );
-				}
-			}
+			return instance;
+		}
+
+		// Add `from` itself, if it is an element.
+		if ( from instanceof ViewElement ) {
+			instance.add( from, ViewConsumable.consumablesFromElement( from ) );
+		}
+
+		if ( from instanceof ViewDocumentFragment ) {
+			instance.add( from );
+		}
+
+		for ( let child of from.getChildren() ) {
+			instance = ViewConsumable.createFrom( child, instance );
 		}
+
+		return instance;
 	}
 }
 
 /**
- * Class used for handling consumption of view {@link module:engine/view/element~Element elements},
- * {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}.
- * Element's name and its parts (attributes, classes and styles) can be consumed separately. Consuming an element's name
- * does not consume its attributes, classes and styles.
- * To add items for consumption use {@link module:engine/conversion/viewconsumable~ViewConsumable#add add method}.
- * To test items use {@link module:engine/conversion/viewconsumable~ViewConsumable#test test method}.
- * To consume items use {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume method}.
- * To revert already consumed items use {@link module:engine/conversion/viewconsumable~ViewConsumable#revert revert method}.
+ * This is a private helper-class for {@link module:engine/conversion/viewconsumable~ViewConsumable}.
+ * It represents and manipulates consumable parts of a single {@link module:engine/view/element~Element}.
  *
- *		viewConsumable.add( element, { name: true } ); // Adds element's name as ready to be consumed.
- *		viewConsumable.add( textNode ); // Adds text node for consumption.
- *		viewConsumable.add( docFragment ); // Adds document fragment for consumption.
- *		viewConsumable.test( element, { name: true }  ); // Tests if element's name can be consumed.
- *		viewConsumable.test( textNode ); // Tests if text node can be consumed.
- *		viewConsumable.test( docFragment ); // Tests if document fragment can be consumed.
- *		viewConsumable.consume( element, { name: true }  ); // Consume element's name.
- *		viewConsumable.consume( textNode ); // Consume text node.
- *		viewConsumable.consume( docFragment ); // Consume document fragment.
- *		viewConsumable.revert( element, { name: true }  ); // Revert already consumed element's name.
- *		viewConsumable.revert( textNode ); // Revert already consumed text node.
- *		viewConsumable.revert( docFragment ); // Revert already consumed document fragment.
+ * @private
  */
-export default class ViewConsumable {
+class ViewElementConsumables {
 
 	/**
-	 * Creates new ViewConsumable.
+	 * Creates ViewElementConsumables instance.
 	 */
-	constructor() {
+	constructor()  {
 		/**
-		 * Map of consumable elements. If {@link module:engine/view/element~Element element} is used as a key,
-		 * {@link module:engine/conversion/viewconsumable~ViewElementConsumables ViewElementConsumables} instance is stored as value.
-		 * For {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}
-		 * boolean value is stored as value.
+		 * Flag indicating if name of the element can be consumed.
 		 *
-		 * @protected
-		 * @member {Map.<module:engine/conversion/viewconsumable~ViewElementConsumables|Boolean>}
-		*/
-		this._consumables = new Map();
+		 * @private
+		 * @member {Boolean}
+		 */
+		this._canConsumeName = null;
+
+		/**
+		 * Contains maps of element's consumables: attributes, classes and styles.
+		 *
+		 * @private
+		 * @member {Object}
+		 */
+		this._consumables = {
+			attribute: new Map(),
+			style: new Map(),
+			class: new Map()
+		};
 	}
 
 	/**
-	 * Adds {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
-	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} as ready to be consumed.
+	 * Adds consumable parts of the {@link module:engine/view/element~Element view element}.
+	 * Element's name itself can be marked to be consumed (when element's name is consumed its attributes, classes and
+	 * styles still could be consumed):
 	 *
-	 *		viewConsumable.add( p, { name: true } ); // Adds element's name to consume.
-	 *		viewConsumable.add( p, { attribute: 'name' } ); // Adds element's attribute.
-	 *		viewConsumable.add( p, { class: 'foobar' } ); // Adds element's class.
-	 *		viewConsumable.add( p, { style: 'color' } ); // Adds element's style
-	 *		viewConsumable.add( p, { attribute: 'name', style: 'color' } ); // Adds attribute and style.
-	 *		viewConsumable.add( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be provided.
-	 *		viewConsumable.add( textNode ); // Adds text node to consume.
-	 *		viewConsumable.add( docFragment ); // Adds document fragment to consume.
+	 *		consumables.add( { name: true } );
 	 *
-	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
-	 * attribute is provided - it should be handled separately by providing actual style/class.
+	 * Attributes classes and styles:
 	 *
-	 *		viewConsumable.add( p, { attribute: 'style' } ); // This call will throw an exception.
-	 *		viewConsumable.add( p, { style: 'color' } ); // This is properly handled style.
+	 *		consumables.add( { attribute: 'title', class: 'foo', style: 'color' } );
+	 *		consumables.add( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
 	 *
-	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
-	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
-	 * @param {Boolean} consumables.name If set to true element's name will be included.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
+	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
+	 * attribute is provided - it should be handled separately by providing `style` and `class` in consumables object.
+	 *
+	 * @param {Object} consumables Object describing which parts of the element can be consumed.
+	 * @param {Boolean} consumables.name If set to `true` element's name will be added as consumable.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to add as consumable.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names to add as consumable.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names to add as consumable.
 	 */
-	add( element, consumables ) {
-		let elementConsumables;
-
-		// For text nodes and document fragments just mark them as consumable.
-		if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
-			this._consumables.set( element, true );
-
-			return;
+	add( consumables ) {
+		if ( consumables.name ) {
+			this._canConsumeName = true;
 		}
 
-		// For elements create new ViewElementConsumables or update already existing one.
-		if ( !this._consumables.has( element ) ) {
-			elementConsumables = new ViewElementConsumables();
-			this._consumables.set( element, elementConsumables );
-		} else {
-			elementConsumables = this._consumables.get( element );
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				this._add( type, consumables[ type ] );
+			}
 		}
-
-		elementConsumables.add( consumables );
 	}
 
 	/**
-	 * Tests if {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
-	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} can be consumed.
-	 * It returns `true` when all items included in method's call can be consumed. Returns `false` when
-	 * first already consumed item is found and `null` when first non-consumable item is found.
+	 * Tests if parts of the {@link module:engine/view/node~Node view node} can be consumed.
 	 *
-	 *		viewConsumable.test( p, { name: true } ); // Tests element's name.
-	 *		viewConsumable.test( p, { attribute: 'name' } ); // Tests attribute.
-	 *		viewConsumable.test( p, { class: 'foobar' } ); // Tests class.
-	 *		viewConsumable.test( p, { style: 'color' } ); // Tests style.
-	 *		viewConsumable.test( p, { attribute: 'name', style: 'color' } ); // Tests attribute and style.
-	 *		viewConsumable.test( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be tested.
-	 *		viewConsumable.test( textNode ); // Tests text node.
-	 *		viewConsumable.test( docFragment ); // Tests document fragment.
+	 * Element's name can be tested:
 	 *
-	 * Testing classes and styles as attribute will test if all added classes/styles can be consumed.
+	 *		consumables.test( { name: true } );
 	 *
-	 *		viewConsumable.test( p, { attribute: 'class' } ); // Tests if all added classes can be consumed.
-	 *		viewConsumable.test( p, { attribute: 'style' } ); // Tests if all added styles can be consumed.
+	 * Attributes classes and styles:
 	 *
-	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
-	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
-	 * @param {Boolean} consumables.name If set to true element's name will be included.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
-	 * @returns {Boolean|null} Returns `true` when all items included in method's call can be consumed. Returns `false`
-	 * when first already consumed item is found and `null` when first non-consumable item is found.
+	 *		consumables.test( { attribute: 'title', class: 'foo', style: 'color' } );
+	 *		consumables.test( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
+	 *
+	 * @param {Object} consumables Object describing which parts of the element should be tested.
+	 * @param {Boolean} consumables.name If set to `true` element's name will be tested.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to test.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names to test.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names to test.
+	 * @returns {Boolean|null} `true` when all tested items can be consumed, `null` when even one of the items
+	 * was never marked for consumption and `false` when even one of the items was already consumed.
 	 */
-	test( element, consumables ) {
-		const elementConsumables = this._consumables.get( element );
-
-		if ( elementConsumables === undefined ) {
-			return null;
+	test( consumables ) {
+		// Check if name can be consumed.
+		if ( consumables.name && !this._canConsumeName ) {
+			return this._canConsumeName;
 		}
 
-		// For text nodes and document fragments return stored boolean value.
-		if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
-			return elementConsumables;
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				const value = this._test( type, consumables[ type ] );
+
+				if ( value !== true ) {
+					return value;
+				}
+			}
 		}
 
-		// For elements test consumables object.
-		return elementConsumables.test( consumables );
+		// Return true only if all can be consumed.
+		return true;
 	}
 
 	/**
-	 * Consumes {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
-	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
-	 * It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
+	 * Consumes parts of {@link module:engine/view/element~Element view element}. This function does not check if consumable item
+	 * is already consumed - it consumes all consumable items provided.
+	 * Element's name can be consumed:
 	 *
-	 *		viewConsumable.consume( p, { name: true } ); // Consumes element's name.
-	 *		viewConsumable.consume( p, { attribute: 'name' } ); // Consumes element's attribute.
-	 *		viewConsumable.consume( p, { class: 'foobar' } ); // Consumes element's class.
-	 *		viewConsumable.consume( p, { style: 'color' } ); // Consumes element's style.
-	 *		viewConsumable.consume( p, { attribute: 'name', style: 'color' } ); // Consumes attribute and style.
-	 *		viewConsumable.consume( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be consumed.
-	 *		viewConsumable.consume( textNode ); // Consumes text node.
-	 *		viewConsumable.consume( docFragment ); // Consumes document fragment.
+	 *		consumables.consume( { name: true } );
 	 *
-	 * Consuming classes and styles as attribute will test if all added classes/styles can be consumed.
+	 * Attributes classes and styles:
 	 *
-	 *		viewConsumable.consume( p, { attribute: 'class' } ); // Consume only if all added classes can be consumed.
-	 *		viewConsumable.consume( p, { attribute: 'style' } ); // Consume only if all added styles can be consumed.
+	 *		consumables.consume( { attribute: 'title', class: 'foo', style: 'color' } );
+	 *		consumables.consume( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
 	 *
-	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
-	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
-	 * @param {Boolean} consumables.name If set to true element's name will be included.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
-	 * @returns {Boolean} Returns `true` when all items included in method's call can be consumed,
-	 * otherwise returns `false`.
+	 * @param {Object} consumables Object describing which parts of the element should be consumed.
+	 * @param {Boolean} consumables.name If set to `true` element's name will be consumed.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to consume.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names to consume.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names to consume.
 	 */
-	consume( element, consumables ) {
-		if ( this.test( element, consumables ) ) {
-			if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
-				// For text nodes and document fragments set value to false.
-				this._consumables.set( element, false );
-			} else {
-				// For elements - consume consumables object.
-				this._consumables.get( element ).consume( consumables );
-			}
-
-			return true;
+	consume( consumables ) {
+		if ( consumables.name ) {
+			this._canConsumeName = false;
 		}
 
-		return false;
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				this._consume( type, consumables[ type ] );
+			}
+		}
 	}
 
 	/**
-	 * Reverts {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
-	 * {@link module:engine/view/documentfragment~DocumentFragment document fragment} so they can be consumed once again.
-	 * Method does not revert items that were never previously added for consumption, even if they are included in
-	 * method's call.
+	 * Revert already consumed parts of {@link module:engine/view/element~Element view Element}, so they can be consumed once again.
+	 * Element's name can be reverted:
 	 *
-	 *		viewConsumable.revert( p, { name: true } ); // Reverts element's name.
-	 *		viewConsumable.revert( p, { attribute: 'name' } ); // Reverts element's attribute.
-	 *		viewConsumable.revert( p, { class: 'foobar' } ); // Reverts element's class.
-	 *		viewConsumable.revert( p, { style: 'color' } ); // Reverts element's style.
-	 *		viewConsumable.revert( p, { attribute: 'name', style: 'color' } ); // Reverts attribute and style.
-	 *		viewConsumable.revert( p, { class: [ 'baz', 'bar' ] } ); // Multiple names can be reverted.
-	 *		viewConsumable.revert( textNode ); // Reverts text node.
-	 *		viewConsumable.revert( docFragment ); // Reverts document fragment.
+	 *		consumables.revert( { name: true } );
 	 *
-	 * Reverting classes and styles as attribute will revert all classes/styles that were previously added for
-	 * consumption.
+	 * Attributes classes and styles:
 	 *
-	 *		viewConsumable.revert( p, { attribute: 'class' } ); // Reverts all classes added for consumption.
-	 *		viewConsumable.revert( p, { attribute: 'style' } ); // Reverts all styles added for consumption.
+	 *		consumables.revert( { attribute: 'title', class: 'foo', style: 'color' } );
+	 *		consumables.revert( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
 	 *
-	 * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
-	 * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
-	 * @param {Boolean} consumables.name If set to true element's name will be included.
-	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
-	 * @param {String|Array.<String>} consumables.class Class name or array of class names.
-	 * @param {String|Array.<String>} consumables.style Style name or array of style names.
+	 * @param {Object} consumables Object describing which parts of the element should be reverted.
+	 * @param {Boolean} consumables.name If set to `true` element's name will be reverted.
+	 * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to revert.
+	 * @param {String|Array.<String>} consumables.class Class name or array of class names to revert.
+	 * @param {String|Array.<String>} consumables.style Style name or array of style names to revert.
 	 */
-	revert( element, consumables ) {
-		const elementConsumables = this._consumables.get( element );
+	revert( consumables ) {
+		if ( consumables.name ) {
+			this._canConsumeName = true;
+		}
 
-		if ( elementConsumables !== undefined ) {
-			if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
-				// For text nodes and document fragments - set consumable to true.
-				this._consumables.set( element, true );
-			} else {
-				// For elements - revert items from consumables object.
-				elementConsumables.revert( consumables );
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				this._revert( type, consumables[ type ] );
 			}
 		}
 	}
 
 	/**
-	 * Creates consumable object from {@link module:engine/view/element~Element view element}. Consumable object will include
-	 * element's name and all its attributes, classes and styles.
+	 * Helper method that adds consumables of a given type: attribute, class or style.
 	 *
-	 * @static
-	 * @param {module:engine/view/element~Element} element
-	 * @returns {Object} consumables
+	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
+	 * type is provided - it should be handled separately by providing actual style/class type.
+	 *
+	 * @private
+	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
+	 * @param {String|Array.<String>} item Consumable item or array of items.
 	 */
-	static consumablesFromElement( element ) {
-		const consumables = {
-			name: true,
-			attribute: [],
-			class: [],
-			style: []
-		};
-
-		const attributes = element.getAttributeKeys();
+	_add( type, item ) {
+		const items = isArray( item ) ? item : [ item ];
+		const consumables = this._consumables[ type ];
 
-		for ( let attribute of attributes ) {
-			// Skip classes and styles - will be added separately.
-			if ( attribute == 'style' || attribute == 'class' ) {
-				continue;
+		for ( let name of items ) {
+			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
+				/**
+				 * Class and style attributes should be handled separately.
+				 *
+				 * @error viewconsumable-invalid-attribute
+				 */
+				throw new CKEditorError( 'viewconsumable-invalid-attribute: Classes and styles should be handled separately.' );
 			}
 
-			consumables.attribute.push( attribute );
+			consumables.set( name, true );
 		}
+	}
 
-		const classes = element.getClassNames();
+	/**
+	 * Helper method that tests consumables of a given type: attribute, class or style.
+	 *
+	 * @private
+	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
+	 * @param {String|Array.<String>} item Consumable item or array of items.
+	 * @returns {Boolean|null} Returns `true` if all items can be consumed, `null` when one of the items cannot be
+	 * consumed and `false` when one of the items is already consumed.
+	 */
+	_test( type, item ) {
+		const items = isArray( item ) ? item : [ item ];
+		const consumables = this._consumables[ type ];
 
-		for ( let className of classes ) {
-			consumables.class.push( className );
-		}
+		for ( let name of items ) {
+			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) )  {
+				// Check all classes/styles if class/style attribute is tested.
+				const value = this._test( name, [ ...this._consumables[ name ].keys() ] );
 
-		const styles = element.getStyleNames();
+				if ( value !== true ) {
+					return value;
+				}
+			} else {
+				const value = consumables.get( name );
+				// Return null if attribute is not found.
+				if ( value === undefined ) {
+					return null;
+				}
 
-		for ( let style of styles ) {
-			consumables.style.push( style );
+				if ( !value ) {
+					return false;
+				}
+			}
 		}
 
-		return consumables;
+		return true;
 	}
 
 	/**
-	 * Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from
-	 * {@link module:engine/view/element~Element element} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
-	 * Instance will contain all elements, child nodes, attributes, styles and classes added for consumption.
+	 * Helper method that consumes items of a given type: attribute, class or style.
 	 *
-	 * @static
-	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} from View element or document fragment
-	 * from which `ViewConsumable` will be created.
-	 * @param {module:engine/conversion/viewconsumable~ViewConsumable} [instance] If provided, given `ViewConsumable` instance will be used
-	 * to add all consumables. It will be returned instead of a new instance.
+	 * @private
+	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
+	 * @param {String|Array.<String>} item Consumable item or array of items.
 	 */
-	static createFrom( from, instance ) {
-		if ( !instance ) {
-			instance = new ViewConsumable();
-		}
-
-		if ( from instanceof ViewText ) {
-			instance.add( from );
+	_consume( type, item ) {
+		const items = isArray( item ) ? item : [ item ];
+		const consumables = this._consumables[ type ];
 
-			return instance;
+		for ( let name of items ) {
+			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
+				// If class or style is provided for consumption - consume them all.
+				this._consume( name, [ ...this._consumables[ name ].keys() ] );
+			} else {
+				consumables.set( name, false );
+			}
 		}
+	}
 
-		// Add `from` itself, if it is an element.
-		if ( from instanceof ViewElement ) {
-			instance.add( from, ViewConsumable.consumablesFromElement( from ) );
-		}
+	/**
+	 * Helper method that reverts items of a given type: attribute, class or style.
+	 *
+	 * @private
+	 * @param {String} type Type of the consumable item: `attribute`, `class` or , `style`.
+	 * @param {String|Array.<String>} item Consumable item or array of items.
+	 */
+	_revert( type, item ) {
+		const items = isArray( item ) ? item : [ item ];
+		const consumables = this._consumables[ type ];
 
-		if ( from instanceof ViewDocumentFragment ) {
-			instance.add( from );
-		}
+		for ( let name of items ) {
+			if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
+				// If class or style is provided for reverting - revert them all.
+				this._revert( name, [ ...this._consumables[ name ].keys() ] );
+			} else {
+				const value = consumables.get( name );
 
-		for ( let child of from.getChildren() ) {
-			instance = ViewConsumable.createFrom( child, instance );
+				if ( value === false ) {
+					consumables.set( name, true );
+				}
+			}
 		}
-
-		return instance;
 	}
 }

+ 3 - 3
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -44,7 +44,7 @@ import extend from '../../utils/lib/lodash/extend.js';
  * Examples of providing callbacks for `ViewConversionDispatcher`:
  *
  *		// Converter for paragraphs (<p>).
- *		viewDispatcher.on( 'element:p', ( data, consumable, conversionApi ) => {
+ *		viewDispatcher.on( 'element:p', ( evt, data, consumable, conversionApi ) => {
  *			const paragraph = new ModelElement( 'paragraph' );
  *			const schemaQuery = {
  *				name: 'paragraph',
@@ -64,7 +64,7 @@ import extend from '../../utils/lib/lodash/extend.js';
  *		} );
  *
  *		// Converter for links (<a>).
- *		viewDispatcher.on( 'element:a', ( data, consumable, conversionApi ) => {
+ *		viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => {
  *			if ( consumable.consume( data.input, { name: true, attributes: [ 'href' ] } ) ) {
  *				// <a> element is inline and is represented by an attribute in the model.
  *				// This is why we are not updating `context` property.
@@ -77,7 +77,7 @@ import extend from '../../utils/lib/lodash/extend.js';
  *						inside: data.context
  *					};
  *
- *					if ( conversionApi.schema.checkQuery( schemaQuery ) ) {
+ *					if ( conversionApi.schema.check( schemaQuery ) ) {
  *						item.setAttribute( 'link', data.input.getAttribute( 'href' ) );
  *					}
  *				}

+ 37 - 0
packages/ckeditor5-engine/src/model/delta/basic-transformations.js

@@ -27,6 +27,7 @@ import WeakInsertDelta from './weakinsertdelta.js';
 import WrapDelta from './wrapdelta.js';
 import UnwrapDelta from './unwrapdelta.js';
 import RenameDelta from './renamedelta.js';
+import RemoveDelta from './removedelta.js';
 
 import compareArrays from '../../../utils/comparearrays.js';
 
@@ -390,6 +391,42 @@ addTransformationCase( SplitDelta, RenameDelta, ( a, b ) => {
 	return [ a ];
 } );
 
+// Add special case for RemoveDelta x SplitDelta transformation.
+addTransformationCase( RemoveDelta, SplitDelta, ( a, b, isStrong ) => {
+	const deltas = defaultTransform( a, b, isStrong );
+	const insertPosition = b._cloneOperation.position;
+
+	// In case if `defaultTransform` returned more than one delta.
+	for ( let delta of deltas ) {
+		for ( let operation of delta.operations ) {
+			const rangeEnd = operation.sourcePosition.getShiftedBy( operation.howMany );
+
+			if ( rangeEnd.isEqual( insertPosition ) ) {
+				operation.howMany += 1;
+			}
+		}
+	}
+
+	return deltas;
+} );
+
+// Add special case for SplitDelta x RemoveDelta transformation.
+addTransformationCase( SplitDelta, RemoveDelta, ( a, b, isStrong ) => {
+	b = b.clone();
+
+	const insertPosition = a._cloneOperation.position;
+
+	for ( let operation of b.operations ) {
+		const rangeEnd = operation.sourcePosition.getShiftedBy( operation.howMany );
+
+		if ( rangeEnd.isEqual( insertPosition ) ) {
+			operation.howMany += 1;
+		}
+	}
+
+	return defaultTransform( a, b, isStrong );
+} );
+
 // Helper function for `AttributeDelta` class transformations.
 // Creates an attribute delta that sets attribute from given `attributeDelta` on nodes from given `weakInsertDelta`.
 function _getComplementaryAttrDelta( weakInsertDelta, attributeDelta ) {

+ 3 - 1
packages/ckeditor5-engine/src/model/delta/transform.js

@@ -191,8 +191,10 @@ export function getTransformationCase( a, b ) {
 		const cases = specialCases.keys();
 
 		for ( let caseClass of cases ) {
-			if ( a instanceof caseClass ) {
+			if ( a instanceof caseClass && specialCases.get( caseClass ).get( b.constructor ) ) {
 				casesA = specialCases.get( caseClass );
+
+				break;
 			}
 		}
 	}

+ 3 - 2
packages/ckeditor5-engine/src/model/operation/attributeoperation.js

@@ -110,17 +110,18 @@ export default class AttributeOperation extends Operation {
 	_execute() {
 		// Validation.
 		for ( let item of this.range.getItems() ) {
-			if ( this.oldValue !== null && item.getAttribute( this.key ) !== this.oldValue ) {
+			if ( this.oldValue !== null && !isEqual( item.getAttribute( this.key ), this.oldValue ) ) {
 				/**
 				 * Changed node has different attribute value than operation's old attribute value.
 				 *
 				 * @error operation-attribute-wrong-old-value
 				 * @param {module:engine/model/item~Item} item
 				 * @param {String} key
+				 * @param {*} value
 				 */
 				throw new CKEditorError(
 					'attribute-operation-wrong-old-value: Changed node has different attribute value than operation\'s old attribute value.',
-					{ item: item, key: this.key }
+					{ item: item, key: this.key, value: this.oldValue }
 				);
 			}
 

+ 8 - 1
packages/ckeditor5-engine/src/model/operation/removeoperation.js

@@ -120,7 +120,14 @@ export default class RemoveOperation extends MoveOperation {
 			const graveyard = this.targetPosition.root;
 			const holderElement = new Element( '$graveyardHolder' );
 
-			graveyard.insertChildren( this.targetPosition.path[ 0 ], holderElement );
+			graveyard.insertChildren( this._holderElementOffset, holderElement );
+
+			// If the operation removes nodes that are already in graveyard, it may happen that
+			// the operation's source position is invalidated by inserting new holder element into the graveyard.
+			// If that's the case, we need to fix source position path.
+			if ( this.sourcePosition.root == graveyard && this.sourcePosition.path[ 0 ] >= this._holderElementOffset ) {
+				this.sourcePosition.path[ 0 ]++;
+			}
 		}
 
 		// Then, execute as a move operation.

+ 6 - 0
packages/ckeditor5-engine/src/model/range.js

@@ -442,6 +442,12 @@ export default class Range {
 	 * @returns {Array.<module:engine/model/range~Range>} Result of the transformation.
 	 */
 	_getTransformedByMove( sourcePosition, targetPosition, howMany, spread, isSticky = false ) {
+		if ( this.isCollapsed ) {
+			const newPos = this.start._getTransformedByMove( sourcePosition, targetPosition, howMany, true, true );
+
+			return [ new Range( newPos ) ];
+		}
+
 		let result;
 
 		const moveRange = new Range( sourcePosition, sourcePosition.getShiftedBy( howMany ) );

+ 212 - 212
packages/ckeditor5-engine/src/model/schema.js

@@ -14,218 +14,6 @@ import isArray from '../../utils/lib/lodash/isArray.js';
 import isString from '../../utils/lib/lodash/isString.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
-/**
- * SchemaItem is a singular registry item in {@link module:engine/model/schema~Schema} that groups and holds allow/disallow rules for
- * one entity. This class is used internally in {@link module:engine/model/schema~Schema} and should not be used outside it.
- *
- * @see module:engine/model/schema~Schema
- * @protected
- */
-export class SchemaItem {
-	/**
-	 * Creates SchemaItem instance.
-	 *
-	 * @param {module:engine/model/schema~Schema} schema Schema instance that owns this item.
-	 */
-	constructor( schema ) {
-		/**
-		 * Schema instance that owns this item.
-		 *
-		 * @private
-		 * @member {module:engine/model/schema~Schema} module:engine/model/schema~SchemaItem#_schema
-		 */
-		this._schema = schema;
-
-		/**
-		 * Paths in which the entity, represented by this item, is allowed.
-		 *
-		 * @private
-		 * @member {Array} module:engine/model/schema~SchemaItem#_allowed
-		 */
-		this._allowed = [];
-
-		/**
-		 * Paths in which the entity, represented by this item, is disallowed.
-		 *
-		 * @private
-		 * @member {Array} module:engine/model/schema~SchemaItem#_disallowed
-		 */
-		this._disallowed = [];
-
-		/**
-		 * Attributes that are required by the entity represented by this item.
-		 *
-		 * @protected
-		 * @member {Array} module:engine/model/schema~SchemaItem#_requiredAttributes
-		 */
-		this._requiredAttributes = [];
-	}
-
-	/**
-	 * Allows entity, represented by this item, to be in given path.
-	 *
-	 * @param {Array.<String>} path Path in which entity is allowed.
-	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
-	 */
-	allow( path, attributes ) {
-		this._addPath( '_allowed', path, attributes );
-	}
-
-	/**
-	 * Disallows entity, represented by this item, to be in given path.
-	 *
-	 * @param {Array.<String>} path Path in which entity is disallowed.
-	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have an attribute(s) with this key.
-	 */
-	disallow( path, attributes ) {
-		this._addPath( '_disallowed', path, attributes );
-	}
-
-	/**
-	 * Specifies that the entity, to be valid, requires given attributes set. It is possible to register multiple
-	 * different attributes set. If there are more than one attributes set required, the entity will be valid if
-	 * at least one of them is fulfilled.
-	 *
-	 * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
-	 */
-	requireAttributes( attributes ) {
-		this._requiredAttributes.push( attributes );
-	}
-
-	/**
-	 * Adds path to the SchemaItem instance.
-	 *
-	 * @private
-	 * @param {String} member Name of the array member into which the path will be added. Possible values are `_allowed` or `_disallowed`.
-	 * @param {Array.<String>} path Path to add.
-	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
-	 */
-	_addPath( member, path, attributes ) {
-		path = path.slice();
-
-		if ( !isArray( attributes ) ) {
-			attributes = [ attributes ];
-		}
-
-		for ( let attribute of attributes ) {
-			this[ member ].push( { path, attribute } );
-		}
-	}
-
-	/**
-	 * Returns all paths of given type that were previously registered in the item.
-	 *
-	 * @private
-	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
-	 * @param {String} [attribute] If set, only paths registered for given attribute will be returned.
-	 * @returns {Array} Paths registered in the item.
-	 */
-	_getPaths( type, attribute ) {
-		const source = type === 'allow' ? this._allowed : this._disallowed;
-		const paths = [];
-
-		for ( let item of source ) {
-			if ( item.attribute === attribute ) {
-				paths.push( item.path );
-			}
-		}
-
-		return paths;
-	}
-
-	/**
-	 * Checks whether given set of attributes fulfills required attributes of this item.
-	 *
-	 * @protected
-	 * @see module:engine/model/schema~SchemaItem#requireAttributes
-	 * @param {Array.<String>} attributesToCheck Attributes to check.
-	 * @returns {Boolean} `true` if given set or attributes fulfills required attributes, `false` otherwise.
-	 */
-	_checkRequiredAttributes( attributesToCheck ) {
-		let found = true;
-
-		for ( let attributeSet of this._requiredAttributes ) {
-			found = true;
-
-			for ( let attribute of attributeSet ) {
-				if ( attributesToCheck.indexOf( attribute ) == -1 ) {
-					found = false;
-					break;
-				}
-			}
-
-			if ( found ) {
-				break;
-			}
-		}
-
-		return found;
-	}
-
-	/**
-	 * Checks whether this item has any registered path of given type that matches provided path.
-	 *
-	 * @protected
-	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
-	 * @param {Array.<String>} checkPath Path to check.
-	 * @param {String} [attribute] If set, only paths registered for given attribute will be checked.
-	 * @returns {Boolean} `true` if item has any registered matching path, `false` otherwise.
-	 */
-	_hasMatchingPath( type, checkPath, attribute ) {
-		const itemPaths = this._getPaths( type, attribute );
-
-		// We check every path registered (possibly with given attribute) in the item.
-		for ( let itemPath of itemPaths ) {
-			// Pointer to last found item from `itemPath`.
-			let i = 0;
-
-			// Now we have to check every item name from the path to check.
-			for ( let checkName of checkPath ) {
-				// Don't check items that are not registered in schema.
-				if ( !this._schema.hasItem( checkName ) ) {
-					continue;
-				}
-
-				// Every item name is expanded to all names of items that item is extending.
-				// So, if on item path, there is an item that is extended by item from checked path, it will
-				// also be treated as matching.
-				const chain = this._schema._extensionChains.get( checkName );
-
-				// Since our paths have to match in given order, we always check against first item from item path.
-				// So, if item path is: B D E
-				// And checked path is: A B C D E
-				// It will be matching (A won't match, B will match, C won't match, D and E will match)
-				if ( chain.indexOf( itemPath[ i ] ) > -1 ) {
-					// Move pointer as we found element under index `i`.
-					i++;
-				}
-			}
-
-			// If `itemPath` has no items it means that we removed all of them, so we matched all of them.
-			// This means that we found a matching path.
-			if ( i === itemPath.length ) {
-				return true;
-			}
-		}
-
-		return false;
-	}
-
-	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
-	 *
-	 * @returns {Object} Clone of this object with the parent property replaced with its name.
-	 */
-	toJSON() {
-		const json = clone( this );
-
-		// Due to circular references we need to remove parent reference.
-		json._schema = '[model.Schema]';
-
-		return json;
-	}
-}
-
 /**
  * Schema is a definition of the structure of the document. It allows to define which tree model items (element, text, etc.)
  * can be nested within which ones and which attributes can be applied to them. It's created during the run-time of the application,
@@ -564,6 +352,218 @@ export default class Schema {
 	}
 }
 
+/**
+ * SchemaItem is a singular registry item in {@link module:engine/model/schema~Schema} that groups and holds allow/disallow rules for
+ * one entity. This class is used internally in {@link module:engine/model/schema~Schema} and should not be used outside it.
+ *
+ * @see module:engine/model/schema~Schema
+ * @protected
+ */
+export class SchemaItem {
+	/**
+	 * Creates SchemaItem instance.
+	 *
+	 * @param {module:engine/model/schema~Schema} schema Schema instance that owns this item.
+	 */
+	constructor( schema ) {
+		/**
+		 * Schema instance that owns this item.
+		 *
+		 * @private
+		 * @member {module:engine/model/schema~Schema} module:engine/model/schema~SchemaItem#_schema
+		 */
+		this._schema = schema;
+
+		/**
+		 * Paths in which the entity, represented by this item, is allowed.
+		 *
+		 * @private
+		 * @member {Array} module:engine/model/schema~SchemaItem#_allowed
+		 */
+		this._allowed = [];
+
+		/**
+		 * Paths in which the entity, represented by this item, is disallowed.
+		 *
+		 * @private
+		 * @member {Array} module:engine/model/schema~SchemaItem#_disallowed
+		 */
+		this._disallowed = [];
+
+		/**
+		 * Attributes that are required by the entity represented by this item.
+		 *
+		 * @protected
+		 * @member {Array} module:engine/model/schema~SchemaItem#_requiredAttributes
+		 */
+		this._requiredAttributes = [];
+	}
+
+	/**
+	 * Allows entity, represented by this item, to be in given path.
+	 *
+	 * @param {Array.<String>} path Path in which entity is allowed.
+	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
+	 */
+	allow( path, attributes ) {
+		this._addPath( '_allowed', path, attributes );
+	}
+
+	/**
+	 * Disallows entity, represented by this item, to be in given path.
+	 *
+	 * @param {Array.<String>} path Path in which entity is disallowed.
+	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have an attribute(s) with this key.
+	 */
+	disallow( path, attributes ) {
+		this._addPath( '_disallowed', path, attributes );
+	}
+
+	/**
+	 * Specifies that the entity, to be valid, requires given attributes set. It is possible to register multiple
+	 * different attributes set. If there are more than one attributes set required, the entity will be valid if
+	 * at least one of them is fulfilled.
+	 *
+	 * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
+	 */
+	requireAttributes( attributes ) {
+		this._requiredAttributes.push( attributes );
+	}
+
+	/**
+	 * Adds path to the SchemaItem instance.
+	 *
+	 * @private
+	 * @param {String} member Name of the array member into which the path will be added. Possible values are `_allowed` or `_disallowed`.
+	 * @param {Array.<String>} path Path to add.
+	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
+	 */
+	_addPath( member, path, attributes ) {
+		path = path.slice();
+
+		if ( !isArray( attributes ) ) {
+			attributes = [ attributes ];
+		}
+
+		for ( let attribute of attributes ) {
+			this[ member ].push( { path, attribute } );
+		}
+	}
+
+	/**
+	 * Returns all paths of given type that were previously registered in the item.
+	 *
+	 * @private
+	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
+	 * @param {String} [attribute] If set, only paths registered for given attribute will be returned.
+	 * @returns {Array} Paths registered in the item.
+	 */
+	_getPaths( type, attribute ) {
+		const source = type === 'allow' ? this._allowed : this._disallowed;
+		const paths = [];
+
+		for ( let item of source ) {
+			if ( item.attribute === attribute ) {
+				paths.push( item.path );
+			}
+		}
+
+		return paths;
+	}
+
+	/**
+	 * Checks whether given set of attributes fulfills required attributes of this item.
+	 *
+	 * @protected
+	 * @see module:engine/model/schema~SchemaItem#requireAttributes
+	 * @param {Array.<String>} attributesToCheck Attributes to check.
+	 * @returns {Boolean} `true` if given set or attributes fulfills required attributes, `false` otherwise.
+	 */
+	_checkRequiredAttributes( attributesToCheck ) {
+		let found = true;
+
+		for ( let attributeSet of this._requiredAttributes ) {
+			found = true;
+
+			for ( let attribute of attributeSet ) {
+				if ( attributesToCheck.indexOf( attribute ) == -1 ) {
+					found = false;
+					break;
+				}
+			}
+
+			if ( found ) {
+				break;
+			}
+		}
+
+		return found;
+	}
+
+	/**
+	 * Checks whether this item has any registered path of given type that matches provided path.
+	 *
+	 * @protected
+	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
+	 * @param {Array.<String>} checkPath Path to check.
+	 * @param {String} [attribute] If set, only paths registered for given attribute will be checked.
+	 * @returns {Boolean} `true` if item has any registered matching path, `false` otherwise.
+	 */
+	_hasMatchingPath( type, checkPath, attribute ) {
+		const itemPaths = this._getPaths( type, attribute );
+
+		// We check every path registered (possibly with given attribute) in the item.
+		for ( let itemPath of itemPaths ) {
+			// Pointer to last found item from `itemPath`.
+			let i = 0;
+
+			// Now we have to check every item name from the path to check.
+			for ( let checkName of checkPath ) {
+				// Don't check items that are not registered in schema.
+				if ( !this._schema.hasItem( checkName ) ) {
+					continue;
+				}
+
+				// Every item name is expanded to all names of items that item is extending.
+				// So, if on item path, there is an item that is extended by item from checked path, it will
+				// also be treated as matching.
+				const chain = this._schema._extensionChains.get( checkName );
+
+				// Since our paths have to match in given order, we always check against first item from item path.
+				// So, if item path is: B D E
+				// And checked path is: A B C D E
+				// It will be matching (A won't match, B will match, C won't match, D and E will match)
+				if ( chain.indexOf( itemPath[ i ] ) > -1 ) {
+					// Move pointer as we found element under index `i`.
+					i++;
+				}
+			}
+
+			// If `itemPath` has no items it means that we removed all of them, so we matched all of them.
+			// This means that we found a matching path.
+			if ( i === itemPath.length ) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Custom toJSON method to solve child-parent circular dependencies.
+	 *
+	 * @returns {Object} Clone of this object with the parent property replaced with its name.
+	 */
+	toJSON() {
+		const json = clone( this );
+
+		// Due to circular references we need to remove parent reference.
+		json._schema = '[model.Schema]';
+
+		return json;
+	}
+}
+
 /**
  * Object with query used by {@link module:engine/model/schema~Schema} to query schema or add allow/disallow rules to schema.
  *

+ 81 - 6
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -11,10 +11,14 @@ import buildViewConverter  from 'ckeditor5/engine/conversion/buildviewconverter.
 import buildModelConverter  from 'ckeditor5/engine/conversion/buildmodelconverter.js';
 
 import ModelDocumentFragment from 'ckeditor5/engine/model/documentfragment.js';
+import ModelElement from 'ckeditor5/engine/model/element.js';
 import ModelText from 'ckeditor5/engine/model/text.js';
 import ModelSelection from 'ckeditor5/engine/model/selection.js';
 
-import { getData, setData, stringify, parse } from 'ckeditor5/engine/dev-utils/model.js';
+import ViewDocumentFragment from 'ckeditor5/engine/view/documentfragment.js';
+
+import { getData, setData, stringify, parse as parseModel } from 'ckeditor5/engine/dev-utils/model.js';
+import { parse as parseView } from 'ckeditor5/engine/dev-utils/view.js';
 
 import count from 'ckeditor5/utils/count.js';
 
@@ -170,6 +174,46 @@ describe( 'DataController', () => {
 		} );
 	} );
 
+	describe( 'toModel', () => {
+		beforeEach( () => {
+			schema.registerItem( 'paragraph', '$block' );
+
+			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
+		} );
+
+		it( 'should convert content of an element', () => {
+			const viewElement = parseView( '<p>foo</p>' );
+			const modelElement = data.toModel( viewElement );
+
+			expect( modelElement ).to.be.instanceOf( ModelElement );
+			expect( stringify( modelElement ) ).to.equal( '<paragraph>foo</paragraph>' );
+		} );
+
+		it( 'should convert content of an element', () => {
+			const viewFragment = parseView( '<p>foo</p><p>bar</p>' );
+			const modelFragment = data.toModel( viewFragment );
+
+			expect( modelFragment ).to.be.instanceOf( ModelDocumentFragment );
+			expect( stringify( modelFragment ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
+		} );
+
+		it( 'should accept parsing context', () => {
+			modelDocument.createRoot( 'inlineRoot', 'inlineRoot' );
+
+			schema.registerItem( 'inlineRoot' );
+			schema.allow( { name: '$text', inside: 'inlineRoot' } );
+
+			const viewFragment = new ViewDocumentFragment( [ parseView( 'foo' ) ] );
+			const modelFragmentInRoot = data.toModel( viewFragment );
+
+			expect( stringify( modelFragmentInRoot ) ).to.equal( '' );
+
+			const modelFragmentInInlineRoot = data.toModel( viewFragment, 'inlineRoot' );
+
+			expect( stringify( modelFragmentInInlineRoot ) ).to.equal( 'foo' );
+		} );
+	} );
+
 	describe( 'set', () => {
 		it( 'should set data to root', () => {
 			schema.allow( { name: '$text', inside: '$root' } );
@@ -293,26 +337,57 @@ describe( 'DataController', () => {
 	} );
 
 	describe( 'stringify', () => {
-		it( 'should get paragraph with text', () => {
+		beforeEach( () => {
 			modelDocument.schema.registerItem( 'paragraph', '$block' );
 			modelDocument.schema.registerItem( 'div', '$block' );
-			const modelElement = parse( '<div><paragraph>foo</paragraph></div>', modelDocument.schema );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
+		} );
+
+		it( 'should stringify a content of an element', () => {
+			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', modelDocument.schema );
 
 			expect( data.stringify( modelElement ) ).to.equal( '<p>foo</p>' );
 		} );
+
+		it( 'should stringify a content of a document fragment', () => {
+			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', modelDocument.schema );
+
+			expect( data.stringify( modelDocumentFragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
+		} );
 	} );
 
 	describe( 'toView', () => {
-		it( 'should get view element P with text', () => {
+		beforeEach( () => {
 			modelDocument.schema.registerItem( 'paragraph', '$block' );
 			modelDocument.schema.registerItem( 'div', '$block' );
-			const modelElement = parse( '<div><paragraph>foo</paragraph></div>', modelDocument.schema );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
+		} );
+
+		it( 'should convert a content of an element', () => {
+			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', modelDocument.schema );
+
+			const viewDocumentFragment = data.toView( modelElement );
+
+			expect( viewDocumentFragment ).to.be.instanceOf( ViewDocumentFragment );
+
+			const viewElement = viewDocumentFragment.getChild( 0 );
+
+			expect( viewElement.name ).to.equal( 'p' );
+			expect( viewElement.childCount ).to.equal( 1 );
+			expect( viewElement.getChild( 0 ).data ).to.equal( 'foo' );
+		} );
+
+		it( 'should convert a document fragment', () => {
+			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', modelDocument.schema );
+
+			const viewDocumentFragment = data.toView( modelDocumentFragment );
+
+			expect( viewDocumentFragment ).to.be.instanceOf( ViewDocumentFragment );
+			expect( viewDocumentFragment ).to.have.property( 'childCount', 2 );
 
-			const viewElement = data.toView( modelElement ).getChild( 0 );
+			const viewElement = viewDocumentFragment.getChild( 0 );
 
 			expect( viewElement.name ).to.equal( 'p' );
 			expect( viewElement.childCount ).to.equal( 1 );

+ 63 - 12
packages/ckeditor5-engine/tests/model/delta/transform/removedelta.js

@@ -10,13 +10,15 @@ import transformations from 'ckeditor5/engine/model/delta/basic-transformations.
 
 import transform from 'ckeditor5/engine/model/delta/transform.js';
 
+import Element from 'ckeditor5/engine/model/element.js';
 import Position from 'ckeditor5/engine/model/position.js';
 import Range from 'ckeditor5/engine/model/range.js';
 
-import RemoveDelta from 'ckeditor5/engine/model/delta/movedelta.js';
+import RemoveDelta from 'ckeditor5/engine/model/delta/removedelta.js';
 import SplitDelta from 'ckeditor5/engine/model/delta/splitdelta.js';
 
 import MoveOperation from 'ckeditor5/engine/model/operation/moveoperation.js';
+import RemoveOperation from 'ckeditor5/engine/model/operation/removeoperation.js';
 
 import { getNodesAndText, jsonParseStringify } from 'tests/engine/model/_utils/utils.js';
 
@@ -25,7 +27,8 @@ import {
 	expectDelta,
 	getFilledDocument,
 	getMergeDelta,
-	getRemoveDelta
+	getRemoveDelta,
+	getSplitDelta
 } from 'tests/engine/model/delta/transform/_utils/utils.js';
 
 describe( 'transform', () => {
@@ -39,18 +42,11 @@ describe( 'transform', () => {
 	} );
 
 	describe( 'RemoveDelta by', () => {
-		let removeDelta;
-
-		beforeEach( () => {
-			let sourcePosition = new Position( root, [ 3, 3, 3 ] );
-			let howMany = 1;
-
-			removeDelta = getRemoveDelta( sourcePosition, howMany, baseVersion );
-		} );
-
 		describe( 'MergeDelta', () => {
 			it( 'node on the right side of merge was removed', () => {
 				// This special case should be handled by MoveDelta x MergeDelta special case.
+				let sourcePosition = new Position( root, [ 3, 3, 3 ] );
+				let removeDelta = getRemoveDelta( sourcePosition, 1, baseVersion );
 
 				let mergePosition = new Position( root, [ 3, 3, 3 ] );
 				let mergeDelta = getMergeDelta( mergePosition, 4, 12, baseVersion );
@@ -94,7 +90,6 @@ describe( 'transform', () => {
 				} );
 
 				// Test if deltas do what they should after applying transformed delta.
-
 				applyDelta( mergeDelta, doc );
 				applyDelta( transformed[ 0 ], doc );
 				applyDelta( transformed[ 1 ], doc );
@@ -105,5 +100,61 @@ describe( 'transform', () => {
 				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIV' );
 			} );
 		} );
+
+		describe( 'SplitDelta', () => {
+			it( 'node inside the removed range was a node that has been split', () => {
+				let sourcePosition = new Position( root, [ 3, 3, 1 ] );
+				let removeDelta = getRemoveDelta( sourcePosition, 3, baseVersion );
+
+				let splitPosition = new Position( root, [ 3, 3, 2, 2 ] );
+				let nodeCopy = new Element( 'x' );
+				let splitDelta = getSplitDelta( splitPosition, nodeCopy, 2, baseVersion );
+
+				let transformed = transform( removeDelta, splitDelta );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = splitDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: RemoveDelta,
+					operations: [
+						{
+							type: RemoveOperation,
+							sourcePosition: sourcePosition,
+							howMany: 4,
+							baseVersion: baseVersion
+						}
+					]
+				} );
+			} );
+
+			it( 'last node in the removed range was a node that has been split', () => {
+				let sourcePosition = new Position( root, [ 3, 2 ] );
+				let removeDelta = getRemoveDelta( sourcePosition, 2, baseVersion );
+
+				let splitPosition = new Position( root, [ 3, 3, 2 ] );
+				let nodeCopy = new Element( 'div' );
+				let splitDelta = getSplitDelta( splitPosition, nodeCopy, 2, baseVersion );
+
+				let transformed = transform( removeDelta, splitDelta );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = splitDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: RemoveDelta,
+					operations: [
+						{
+							type: RemoveOperation,
+							sourcePosition: sourcePosition,
+							howMany: 3,
+							baseVersion: baseVersion
+						}
+					]
+				} );
+			} );
+		} );
 	} );
 } );

+ 81 - 1
packages/ckeditor5-engine/tests/model/delta/transform/splitdelta.js

@@ -18,11 +18,13 @@ import Delta from 'ckeditor5/engine/model/delta/delta.js';
 import SplitDelta from 'ckeditor5/engine/model/delta/splitdelta.js';
 import AttributeDelta from 'ckeditor5/engine/model/delta/attributedelta.js';
 import RenameDelta from 'ckeditor5/engine/model/delta/renamedelta.js';
+import RemoveDelta from 'ckeditor5/engine/model/delta/removedelta.js';
 
 import InsertOperation from 'ckeditor5/engine/model/operation/insertoperation.js';
 import AttributeOperation from 'ckeditor5/engine/model/operation/attributeoperation.js';
 import ReinsertOperation from 'ckeditor5/engine/model/operation/reinsertoperation.js';
 import MoveOperation from 'ckeditor5/engine/model/operation/moveoperation.js';
+import RemoveOperation from 'ckeditor5/engine/model/operation/removeoperation.js';
 import NoOperation from 'ckeditor5/engine/model/operation/nooperation.js';
 import RenameOperation from 'ckeditor5/engine/model/operation/renameoperation.js';
 
@@ -34,7 +36,8 @@ import {
 	getFilledDocument,
 	getSplitDelta,
 	getWrapDelta,
-	getUnwrapDelta
+	getUnwrapDelta,
+	getRemoveDelta
 } from 'tests/engine/model/delta/transform/_utils/utils.js';
 
 describe( 'transform', () => {
@@ -690,5 +693,82 @@ describe( 'transform', () => {
 				} );
 			} );
 		} );
+
+		describe( 'RemoveDelta', () => {
+			it( 'node inside the removed range was a node that has been split', () => {
+				splitPosition = new Position( root, [ 3, 3, 2, 2 ] );
+				splitDelta = getSplitDelta( splitPosition, new Element( 'x' ), 2, baseVersion );
+
+				let removePosition = new Position( root, [ 3, 3, 1 ] );
+				let removeDelta = getRemoveDelta( removePosition, 3, baseVersion );
+				let removeOperation = removeDelta.operations[ 0 ];
+
+				let transformed = transform( splitDelta, removeDelta );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = removeDelta.operations.length;
+
+				let newInsertPosition = removeOperation.targetPosition.getShiftedBy( 2 );
+				let newMoveSourcePosition = removeOperation.targetPosition.getShiftedBy( 1 );
+				newMoveSourcePosition.path.push( 2 );
+				let newMoveTargetPosition = Position.createAt( newInsertPosition );
+				newMoveTargetPosition.path.push( 0 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: newInsertPosition,
+							baseVersion: baseVersion
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: newMoveSourcePosition,
+							howMany: 2,
+							targetPosition: newMoveTargetPosition,
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
+
+			it( 'last node in the removed range was a node that has been split', () => {
+				let removePosition = new Position( root, [ 3, 3, 2 ] );
+				let removeDelta = getRemoveDelta( removePosition, 2, baseVersion );
+				let removeOperation = removeDelta.operations[ 0 ];
+
+				let transformed = transform( splitDelta, removeDelta );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = removeDelta.operations.length;
+
+				let newInsertPosition = removeOperation.targetPosition.getShiftedBy( 2 );
+				let newMoveSourcePosition = removeOperation.targetPosition.getShiftedBy( 1 );
+				newMoveSourcePosition.path.push( 3 );
+				let newMoveTargetPosition = Position.createAt( newInsertPosition );
+				newMoveTargetPosition.path.push( 0 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: newInsertPosition,
+							baseVersion: baseVersion
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: newMoveSourcePosition,
+							howMany: 9,
+							targetPosition: newMoveTargetPosition,
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
+		} );
 	} );
 } );

+ 16 - 0
packages/ckeditor5-engine/tests/model/operation/attributeoperation.js

@@ -165,6 +165,22 @@ describe( 'AttributeOperation', () => {
 		expect( root.getChild( 0 ).hasAttribute( 'bar' ) ).to.be.true;
 	} );
 
+	it( 'should not throw for non-primitive attribute values', () => {
+		root.insertChildren( 0, new Text( 'x', { foo: [ 'bar', 'xyz' ] } ) );
+
+		expect( () => {
+			doc.applyOperation( wrapInDelta(
+				new AttributeOperation(
+					new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
+					'foo',
+					[ 'bar', 'xyz' ],
+					true,
+					doc.version
+				)
+			) );
+		} ).to.not.throw( Error );
+	} );
+
 	it( 'should create an AttributeOperation as a reverse', () => {
 		let range = new Range( new Position( root, [ 0 ] ), new Position( root, [ 3 ] ) );
 		let operation = new AttributeOperation( range, 'x', 'old', 'new', doc.version );

+ 15 - 0
packages/ckeditor5-engine/tests/model/operation/removeoperation.js

@@ -176,6 +176,21 @@ describe( 'RemoveOperation', () => {
 		expect( root.getChild( 0 ).data ).to.equal( 'bar' );
 	} );
 
+	it( 'should properly remove a node that is already in a graveyard', () => {
+		doc.graveyard.appendChildren( new Element( '$graveyardHolder', {}, [ new Text( 'foo' ) ] ) );
+
+		let position = new Position( doc.graveyard, [ 0, 0 ] );
+		let operation = new RemoveOperation( position, 1, 0 );
+
+		operation.targetPosition.path = [ 0, 0 ];
+
+		doc.applyOperation( wrapInDelta( operation ) );
+
+		expect( doc.graveyard.childCount ).to.equal( 2 );
+		expect( doc.graveyard.getChild( 0 ).getChild( 0 ).data ).to.equal( 'f' );
+		expect( doc.graveyard.getChild( 1 ).getChild( 0 ).data ).to.equal( 'oo' );
+	} );
+
 	describe( 'toJSON', () => {
 		it( 'should create proper json object', () => {
 			const op = new RemoveOperation(

+ 16 - 0
packages/ckeditor5-engine/tests/model/range.js

@@ -555,6 +555,22 @@ describe( 'Range', () => {
 			expect( transformed[ 0 ].start.path ).to.deep.equal( [ 4, 2 ] );
 			expect( transformed[ 0 ].end.path ).to.deep.equal( [ 4, 7 ] );
 		} );
+
+		it( 'should stick to moved range, if the transformed range is collapsed #1', () => {
+			const range = new Range( new Position( root, [ 3, 2 ] ), new Position( root, [ 3, 2 ] ) );
+			const transformed = range._getTransformedByMove( new Position( root, [ 3, 0 ] ), new Position( root, [ 6 ] ), 2 );
+
+			expect( transformed[ 0 ].start.path ).to.deep.equal( [ 8 ] );
+			expect( transformed[ 0 ].end.path ).to.deep.equal( [ 8 ] );
+		} );
+
+		it( 'should stick to moved range, if the transformed range is collapsed #2', () => {
+			const range = new Range( new Position( root, [ 3, 2 ] ), new Position( root, [ 3, 2 ] ) );
+			const transformed = range._getTransformedByMove( new Position( root, [ 3, 2 ] ), new Position( root, [ 6 ] ), 2 );
+
+			expect( transformed[ 0 ].start.path ).to.deep.equal( [ 6 ] );
+			expect( transformed[ 0 ].end.path ).to.deep.equal( [ 6 ] );
+		} );
 	} );
 
 	describe( 'getDifference', () => {