8
0
Quellcode durchsuchen

Merge pull request #294 from ckeditor/t/275

ViewConsumable
Piotr Jasiun vor 9 Jahren
Ursprung
Commit
c3d4cf8cec

+ 591 - 0
packages/ckeditor5-engine/src/treecontroller/viewconsumable.js

@@ -0,0 +1,591 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import isArray from '../../utils/lib/lodash/isArray.js';
+import CKEditorError from '../../utils/ckeditorerror.js';
+import ViewElement from '../treeview/element.js';
+import ViewText from '../treeview/text.js';
+import ViewDocumentFragment from '../treeview/documentfragment.js';
+
+/**
+ * This is a private helper-class for {@link engine.treeController.ViewConsumable}.
+ * It represents and manipulates consumable parts of a single {@link engine.treeView.Element}.
+ *
+ * @private
+ * @memberOf engine.treeController
+ */
+class ViewElementConsumables {
+
+	/**
+	 * Creates ViewElementConsumables instance.
+	 */
+	constructor()  {
+		/**
+		 * Boolean value that indicates if name of the element can be consumed.
+		 *
+		 * @private
+		 * @member {Boolean} engine.treeController.ViewElementConsumables#_canConsumeName
+		 */
+		this._canConsumeName = null;
+
+		/**
+		 * Object containing maps of element's consumables: attributes, classes and styles.
+		 *
+		 * @private
+		 * @member {Object} engine.treeController.ViewElementConsumables#_consumables
+		 */
+		this._consumables = {
+			attribute: new Map(),
+			style: new Map(),
+			class: new Map()
+		};
+	}
+
+	/**
+	 * Adds consumable parts of the {@link engine.treeView.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:
+	 *
+	 *		consumables.add( { attribute: 'title', class: 'foo', style: 'color' } );
+	 *		consumables.add( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
+	 *
+	 * Throws {@link utils.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} consumables.attribute Attribute name or array of attribute names to add as consumable.
+	 * @param {String|Array} consumables.class Class name or array of class names to add as consumable.
+	 * @param {String|Array} consumables.style Style name or array of style names to add as consumable.
+	 */
+	add( consumables ) {
+		if ( consumables.name ) {
+			this._canConsumeName = true;
+		}
+
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				this._add( type, consumables[ type ] );
+			}
+		}
+	}
+
+	/**
+	 * Tests if parts of the {@link engine.treeView.Element view Element} can be consumed. Returns `true` when all tested
+	 * items can be consumed, `null` when even one of the items were never marked for consumption and `false` when even
+	 * one of the items were already 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} consumables.attribute Attribute name or array of attribute names to test.
+	 * @param {String|Array} consumables.class Class name or array of class names to test.
+	 * @param {String|Array} consumables.style Style name or array of style names to test.
+	 * @returns {Boolean|null} Returns `true` when all tested items can be consumed, `null` when even one of the items
+	 * were never marked for consumption and `false` when even one of the items were already consumed.
+	 */
+	test( consumables ) {
+		// Check if name can be consumed.
+		if ( consumables.name && !this._canConsumeName ) {
+			return this._canConsumeName;
+		}
+
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				const value = this._test( type, consumables[ type ] );
+
+				if ( value !== true ) {
+					return value;
+				}
+			}
+		}
+
+		// Return true only if all can be consumed.
+		return true;
+	}
+
+	/**
+	 * Consumes parts of {@link engine.treeView.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:
+	 *
+	 *		consumables.consume( { name: true } );
+	 *
+	 * Attributes classes and styles:
+	 *
+	 *		consumables.consume( { attribute: 'title', class: 'foo', style: 'color' } );
+	 *		consumables.consume( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
+	 *
+	 * @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} consumables.attribute Attribute name or array of attribute names to consume.
+	 * @param {String|Array} consumables.class Class name or array of class names to consume.
+	 * @param {String|Array} consumables.style Style name or array of style names to consume.
+	 */
+	consume( consumables ) {
+		if ( consumables.name ) {
+			this._canConsumeName = false;
+		}
+
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				this._consume( type, consumables[ type ] );
+			}
+		}
+	}
+
+	/**
+	 * Revert already consumed parts of {@link engine.treeView.Element view Element}, so they can be consumed once again.
+	 * Element's name can be reverted:
+	 *
+	 *		consumables.revert( { name: true } );
+	 *
+	 * Attributes classes and styles:
+	 *
+	 *		consumables.revert( { attribute: 'title', class: 'foo', style: 'color' } );
+	 *		consumables.revert( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
+	 *
+	 * @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} consumables.attribute Attribute name or array of attribute names to revert.
+	 * @param {String|Array} consumables.class Class name or array of class names to revert.
+	 * @param {String|Array} consumables.style Style name or array of style names to revert.
+	 */
+	revert( consumables ) {
+		if ( consumables.name ) {
+			this._canConsumeName = true;
+		}
+
+		for ( let type in this._consumables ) {
+			if ( type in consumables ) {
+				this._revert( type, consumables[ type ] );
+			}
+		}
+	}
+
+	/**
+	 * Helper method that adds consumables from one type: attribute, class or style.
+	 *
+	 * Throws {@link utils.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} 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 from one type: attribute, class or style.
+	 *
+	 * @private
+	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
+	 * @param {String|Array} 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 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() ] );
+
+				if ( value !== true ) {
+					return value;
+				}
+			} else {
+				const value = consumables.get( name );
+				// Return null if attribute is not found.
+				if ( value === undefined ) {
+					return null;
+				}
+
+				if ( !value ) {
+					return false;
+				}
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Helper method that consumes items from one type: attribute, class or style.
+	 *
+	 * @private
+	 * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
+	 * @param {String|Array} item Consumable item or array of items.
+	 */
+	_consume( 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' ) ) {
+				// If class or style is provided for consumption - consume them all.
+				this._consume( name, [ ...this._consumables[ name ].keys() ] );
+			} else {
+				consumables.set( name, false );
+			}
+		}
+	}
+
+	/**
+	 * Helper method that reverts items from one type: attribute, class or style.
+	 *
+	 * @private
+	 * @param {String} type Type of the consumable item: `attribute`, `class` or , `style`.
+	 * @param {String|Array} item Consumable item or array of items.
+	 */
+	_revert( 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' ) ) {
+				// 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 ( value === false ) {
+					consumables.set( name, true );
+				}
+			}
+		}
+	}
+}
+
+/**
+ * Class used for handling consumption of view {@link engine.treeView.Element elements},
+ * {@link engine.treeView.Text text nodes} and {@link engine.treeView.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 engine.treeController.ViewConsumable#add add method}.
+ * To test items use {@link engine.treeController.ViewConsumable#test test method}.
+ * To consume items use {@link engine.treeController.ViewConsumable#consume consume method}.
+ * To revert already consumed items use {@link engine.treeController.ViewConsumable#revert revert method}.
+ *
+ *		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.
+ *
+ * @memberOf engine.treeController
+ */
+export default class ViewConsumable {
+
+	/**
+	 * Creates new ViewConsumable.
+	 */
+	constructor() {
+		/**
+		 * Map of consumable elements. If {@link engine.treeView.Element element} is used as a key,
+		 * {@link engine.treeController.ViewElementConsumables ViewElementConsumables} instance is stored as value.
+		 * For {@link engine.treeView.Text text nodes} and {@link engine.treeView.DocumentFragment document fragments}
+		 * boolean value is stored as value.
+		 *
+		 * @protected
+		 * @member {Map.<engine.treeController.ViewElementConsumables|Boolean>} engine.treeController.ViewConsumable#_consumables
+		*/
+		this._consumables = new Map();
+	}
+
+	/**
+	 * Adds {@link engine.treeView.Element view element}, {@link engine.treeView.Text text node} or
+	 * {@link engine.treeView.DocumentFragment document fragment} as ready to 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.
+	 *
+	 * Throws {@link utils.CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
+	 * attribute is provided - it should be handled separately by providing actual style/class.
+	 *
+	 *		viewConsumable.add( p, { attribute: 'style' } ); // This call will throw an exception.
+	 *		viewConsumable.add( p, { style: 'color' } ); // This is properly handled style.
+	 *
+	 * @param {engine.treeView.Element|engine.treeView.Text|engine.treeView.DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link engine.treeView.Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array} consumables.class Class name or array of class names.
+	 * @param {String|Array} consumables.style Style name or array of style names.
+	 */
+	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;
+		}
+
+		// 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 );
+		}
+
+		elementConsumables.add( consumables );
+	}
+
+	/**
+	 * Tests if {@link engine.treeView.Element view element}, {@link engine.treeView.Text text node} or
+	 * {@link engine.treeView.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.
+	 *
+	 *		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.
+	 *
+	 * Testing classes and styles as attribute will test if all added classes/styles can be consumed.
+	 *
+	 *		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 {engine.treeView.Element|engine.treeView.Text|engine.treeView.DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link engine.treeView.Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array} consumables.class Class name or array of class names.
+	 * @param {String|Array} 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.
+	 */
+	test( element, consumables ) {
+		const elementConsumables = this._consumables.get( element );
+
+		if ( elementConsumables === undefined ) {
+			return null;
+		}
+
+		// 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 );
+	}
+
+	/**
+	 * Consumes {@link engine.treeView.Element view element}, {@link engine.treeView.Text text node} or
+	 * {@link engine.treeView.DocumentFragment document fragment}.
+	 * It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
+	 *
+	 *		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.
+	 *
+	 * Consuming classes and styles as attribute will test if all added classes/styles can be consumed.
+	 *
+	 *		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 {engine.treeView.Element|engine.treeView.Text|engine.treeView.DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link engine.treeView.Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array} consumables.class Class name or array of class names.
+	 * @param {String|Array} 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`.
+	 */
+	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;
+	}
+
+	/**
+	 * Reverts {@link engine.treeView.Element view element}, {@link engine.treeView.Text text node} or
+	 * {@link engine.treeView.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.
+	 *
+	 *		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.
+	 *
+	 * Reverting classes and styles as attribute will revert all classes/styles that were previously added for
+	 * consumption.
+	 *
+	 *		viewConsumable.revert( p, { attribute: 'class' } ); // Reverts all classes added for consumption.
+	 *		viewConsumable.revert( p, { attribute: 'style' } ); // Reverts all styles added for consumption.
+	 *
+	 * @param {engine.treeView.Element|engine.treeView.Text|engine.treeView.DocumentFragment} element
+	 * @param {Object} [consumables] Used only if first parameter is {@link engine.treeView.Element view element} instance.
+	 * @param {Boolean} consumables.name If set to true element's name will be included.
+	 * @param {String|Array} consumables.attribute Attribute name or array of attribute names.
+	 * @param {String|Array} consumables.class Class name or array of class names.
+	 * @param {String|Array} consumables.style Style name or array of style names.
+	 */
+	revert( element, consumables ) {
+		const elementConsumables = this._consumables.get( element );
+
+		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 );
+			}
+		}
+	}
+
+	/**
+	 * Creates consumable object from {@link engine.treeView.Element view element}. Consumable object will include
+	 * element's name and all its attributes, classes and styles.
+	 *
+	 * @static
+	 * @param {engine.treeView.Element} element
+	 * @returns {Object} consumables
+	 */
+	static consumablesFromElement( element ) {
+		const consumables = {
+			name: true,
+			attribute: [],
+			class: [],
+			style: []
+		};
+
+		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;
+	}
+
+	/**
+	 * Creates {@link engine.treeController.ViewConsumable ViewConsumable} instance from
+	 * {@link engine.treeView.Element element} or {@link engine.treeView.DocumentFragment document fragment}.
+	 * Instance will contain all elements, child nodes, attributes, styles and classes added for consumption.
+	 *
+	 * @static
+	 * @param {engine.treeView.Element|engine.treeView.DocumentFragment} root
+	 * @param {engine.treeController.ViewConsumable} [instance] Optionally this instance can be used to add all
+	 * consumables from provided root. It will be returned instead of new instance.
+	 */
+	static createFromElement( root, instance ) {
+		if ( !instance ) {
+			instance = new ViewConsumable();
+		}
+
+		// Add root itself if it is an element.
+		if ( root instanceof ViewElement ) {
+			instance.add( root, ViewConsumable.consumablesFromElement( root ) );
+		}
+
+		if ( root instanceof ViewDocumentFragment ) {
+			instance.add( root );
+		}
+
+		for ( let child of root.getChildren() ) {
+			if ( child instanceof ViewText ) {
+				instance.add( child );
+			} else {
+				instance = ViewConsumable.createFromElement( child, instance );
+			}
+		}
+
+		return instance;
+	}
+}

+ 18 - 0
packages/ckeditor5-engine/src/treeview/element.js

@@ -440,6 +440,15 @@ export default class Element extends Node {
 		return true;
 	}
 
+	/**
+	 * Returns iterator that contains all class names.
+	 *
+	 * @returns {Iterator.<String>}
+	 */
+	getClassNames() {
+		return this._classes.keys();
+	}
+
 	/**
 	 * Adds style to the element.
 	 *
@@ -478,6 +487,15 @@ export default class Element extends Node {
 		return this._styles.get( property );
 	}
 
+	/**
+	 * Returns iterator that contains all style names.
+	 *
+	 * @returns {Iterator.<String>}
+	 */
+	getStyleNames() {
+		return this._styles.keys();
+	}
+
 	/**
 	 * Returns true if style keys are present.
 	 * If more then one style property is provided - returns true only when all properties are present.

+ 557 - 0
packages/ckeditor5-engine/tests/treecontroller/viewconsumable.js

@@ -0,0 +1,557 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treecontroller */
+
+'use strict';
+
+import ViewElement from '/ckeditor5/engine/treeview/element.js';
+import ViewText from '/ckeditor5/engine/treeview/text.js';
+import ViewDocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
+import ViewConsumable from '/ckeditor5/engine/treecontroller/viewconsumable.js';
+
+describe( 'ViewConsumable', () => {
+	let viewConsumable;
+	let el;
+
+	beforeEach( () => {
+		viewConsumable = new ViewConsumable();
+		el = new ViewElement( 'p' );
+	} );
+
+	describe( 'add', () => {
+		it( 'should allow to add element name', () => {
+			viewConsumable.add( el, { name: true } );
+
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.true;
+		} );
+
+		it( 'should allow to add text node', () => {
+			const text = new ViewText( 'foobar' );
+			viewConsumable.add( text );
+
+			expect( viewConsumable.test( text ) ).to.be.true;
+		} );
+
+		it( 'should allow to add document fragment', () => {
+			const fragment = new ViewDocumentFragment();
+			viewConsumable.add( fragment );
+			expect( viewConsumable.test( fragment ) ).to.be.true;
+		} );
+
+		it( 'should allow to add attributes classes and styles', () => {
+			viewConsumable.add( el, { attribute: 'href' } );
+			viewConsumable.add( el, { class: 'foobar' } );
+			viewConsumable.add( el, { style: 'color' } );
+
+			expect( viewConsumable.test( el, { attribute: 'href' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { class: 'foobar' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { style: 'color' }  ) ).to.be.true;
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.null;
+		} );
+
+		it( 'should allow to add attributes classes and styles in one call', () => {
+			viewConsumable.add( el, { attribute: 'href', class: 'foobar', style: 'color' } );
+
+			expect( viewConsumable.test( el, { attribute: 'href' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { class: 'foobar' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { style: 'color' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.null;
+		} );
+
+		it( 'should allow to add multiple attributes in one call', () => {
+			viewConsumable.add( el, { attribute: [ 'href', 'target', 'title' ] } );
+
+			expect( viewConsumable.test( el, { attribute: 'href' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'target' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'title' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.null;
+		} );
+
+		it( 'should allow to add multiple classes in one call', () => {
+			viewConsumable.add( el, { class: [ 'foo', 'bar', 'baz' ] } );
+
+			expect( viewConsumable.test( el, { class: 'foo' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { class: 'bar' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { class: 'baz' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.null;
+		} );
+
+		it( 'should allow to add multiple styles in one call', () => {
+			viewConsumable.add( el, { style: [ 'color', 'position', 'top' ] } );
+
+			expect( viewConsumable.test( el, { style: 'color' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { style: 'position' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { style: 'top' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.null;
+		} );
+
+		it( 'should throw if class attribute is added', () => {
+			expect( () => {
+				viewConsumable.add( el, { attribute: 'class' } );
+			} ).to.throw( 'viewconsumable-invalid-attribute' );
+		} );
+
+		it( 'should throw if style attribute is added', () => {
+			expect( () => {
+				viewConsumable.add( el, { attribute: 'style' } );
+			} ).to.throw( 'viewconsumable-invalid-attribute' );
+		} );
+	} );
+
+	describe( 'test', () => {
+		it( 'should test element name', () => {
+			const el2 = new ViewElement( 'p' );
+
+			viewConsumable.add( el, { name: true } );
+
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.true;
+			expect( viewConsumable.test( el2, { name: true } ) ).to.be.null;
+		} );
+
+		it( 'should test text nodes', () => {
+			const text1 = new ViewText();
+			const text2 = new ViewText();
+
+			viewConsumable.add( text1 );
+
+			expect( viewConsumable.test( text1 ) ).to.be.true;
+			expect( viewConsumable.test( text2 ) ).to.be.null;
+		} );
+
+		it( 'should test document fragments', () => {
+			const fragment1 = new ViewDocumentFragment();
+			const fragment2 = new ViewDocumentFragment();
+
+			viewConsumable.add( fragment1 );
+
+			expect( viewConsumable.test( fragment1 ) ).to.be.true;
+			expect( viewConsumable.test( fragment2 ) ).to.be.null;
+		} );
+
+		it( 'should test attributes, classes and styles', () => {
+			const el = new ViewElement( 'p' );
+
+			viewConsumable.add( el, { attribute: 'href', class: 'foobar', style: 'color' } );
+
+			expect( viewConsumable.test( el, { attribute: 'href' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { class: 'foobar' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { style: 'color' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'href', class: 'foobar', style: 'color' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'href', class: 'baz' } ) ).to.be.null;
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.null;
+
+			viewConsumable.consume( el, { style: 'color' } );
+			expect( viewConsumable.test( el, { attribute: 'href', style: 'color' } ) ).to.be.false;
+		} );
+
+		it( 'should allow to test multiple attributes in one call', () => {
+			viewConsumable.add( el, { attribute: [ 'href', 'title', 'target' ] } );
+
+			expect( viewConsumable.test( el, { attribute: [ 'href', 'title', 'target' ] } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: [ 'href', 'title', 'alt' ] } ) ).to.be.null;
+
+			viewConsumable.consume( el, { attribute: 'target' } );
+			expect( viewConsumable.test( el, { attribute: [ 'href', 'title', 'target' ] } ) ).to.be.false;
+		} );
+
+		it( 'should allow to test multiple classes in one call', () => {
+			viewConsumable.add( el, { class: [ 'foo', 'bar', 'baz' ] } );
+
+			expect( viewConsumable.test( el, { class: [ 'foo', 'bar', 'baz' ] } ) ).to.be.true;
+			expect( viewConsumable.test( el, { class: [ 'foo', 'bar', 'qux' ] } ) ).to.be.null;
+
+			viewConsumable.consume( el, { class: 'bar' } );
+			expect( viewConsumable.test( el, { class: [ 'foo', 'bar', 'baz' ] } ) ).to.be.false;
+		} );
+
+		it( 'should allow to test multiple styles in one call', () => {
+			viewConsumable.add( el, { style: [ 'color', 'position', 'top' ] } );
+
+			expect( viewConsumable.test( el, { style: [ 'color', 'position', 'top' ] } ) ).to.be.true;
+			expect( viewConsumable.test( el, { style: [ 'color', 'position', 'left' ] } ) ).to.be.null;
+
+			viewConsumable.consume( el, { style: 'top' } );
+			expect( viewConsumable.test( el, { style: [ 'color', 'position', 'top' ] } ) ).to.be.false;
+		} );
+
+		it( 'should return null if not consumable', () => {
+			expect( viewConsumable.test( el ) ).to.be.null;
+		} );
+
+		it( 'should return false if already consumed', () => {
+			viewConsumable.add( el, { name: true } );
+			viewConsumable.consume( el, { name: true } );
+
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.false;
+		} );
+
+		it( 'should return null if first non-consumable item is found', () => {
+			viewConsumable.add( el, { attribute: 'foo' } );
+
+			expect( viewConsumable.test( el, { attribute: [ 'foo', 'bar' ] } ) ).to.be.null;
+		} );
+
+		it( 'should return false if first already consumed item is found', () => {
+			viewConsumable.add( el, { name: true, attribute: [ 'foo', 'bar' ] } );
+			viewConsumable.consume( el, { attribute: 'bar' } );
+			viewConsumable.consume( el, { name: true } );
+
+			expect( viewConsumable.test( el, { attribute: [ 'foo', 'bar' ] } ) ).to.be.false;
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.false;
+		} );
+
+		it( 'should test all classes if class attribute is tested', () => {
+			viewConsumable.add( el, { class: [ 'foo', 'bar', 'baz' ] } );
+			expect( viewConsumable.test( el, { attribute: 'class' } ) ).to.be.true;
+			expect( viewConsumable.consume( el, { class: 'baz' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'class' } ) ).to.be.false;
+		} );
+
+		it( 'should test all styles if style attribute is tested', () => {
+			viewConsumable.add( el, { style: [ 'color', 'top', 'position' ] } );
+			expect( viewConsumable.test( el, { attribute: 'style' } ) ).to.be.true;
+			expect( viewConsumable.consume( el, { style: 'top' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'style' } ) ).to.be.false;
+		} );
+
+		it( 'should return false when testing class attribute when consumed classes exists', () => {
+			viewConsumable.add( el, { class: [ 'foo', 'baz' ] } );
+			expect( viewConsumable.consume( el, { class: 'baz' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'class' } ) ).to.be.false;
+			expect( viewConsumable.consume( el, { attribute: 'class' } ) ).to.be.false;
+		} );
+
+		it( 'should return false when testing style attribute when consumed styles exists', () => {
+			viewConsumable.add( el, { style: [ 'top', 'left' ] } );
+			expect( viewConsumable.consume( el, { style: 'top' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'style' } ) ).to.be.false;
+			expect( viewConsumable.consume( el, { attribute: 'style' } ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'consume', () => {
+		it( 'should consume element', () => {
+			viewConsumable.add( el, { name: true } );
+			const consumed = viewConsumable.consume( el, { name: true } );
+
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.false;
+			expect( consumed ).to.be.true;
+		} );
+
+		it( 'should consume text node', () => {
+			const text = new ViewText();
+			viewConsumable.add( text );
+			const consumed = viewConsumable.consume( text );
+			expect( consumed ).to.be.true;
+			expect( viewConsumable.test( text ) ).to.be.false;
+			expect( viewConsumable.consume( text ) ).to.be.false;
+		} );
+
+		it( 'should consume document fragment', () => {
+			const fragment = new ViewDocumentFragment();
+			viewConsumable.add( fragment );
+			const consumed = viewConsumable.consume( fragment );
+			expect( consumed ).to.be.true;
+			expect( viewConsumable.test( fragment ) ).to.be.false;
+			expect( viewConsumable.consume( fragment ) ).to.be.false;
+		} );
+
+		it( 'should not consume element not marked for consumption', () => {
+			expect( viewConsumable.consume( el, { name: true } ) ).to.be.false;
+		} );
+
+		it( 'should not consume element already consumed', () => {
+			viewConsumable.add( el, { name: true } );
+
+			expect( viewConsumable.consume( el, { name: true } ) ).to.be.true;
+			expect( viewConsumable.consume( el, { name: true } ) ).to.be.false;
+		} );
+
+		it( 'should consume attributes, classes and styles', () => {
+			viewConsumable.add( el, { class: 'foobar', attribute: 'href', style: 'color' } );
+
+			const consumed1 = viewConsumable.consume( el, { class: 'foobar' } );
+			const consumed2 = viewConsumable.consume( el, { attribute: 'href' } );
+			const consumed3 = viewConsumable.consume( el, { style: 'color' } );
+
+			expect( consumed1 ).to.be.true;
+			expect( consumed2 ).to.be.true;
+			expect( consumed3 ).to.be.true;
+
+			expect( viewConsumable.test( el, { class: 'foobar' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { attribute: 'href' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { style: 'color' } ) ).to.be.false;
+		} );
+
+		it( 'should consume multiple attributes', () => {
+			viewConsumable.add( el, { attribute: [ 'href', 'title', 'name' ] } );
+
+			const consumed = viewConsumable.consume( el, { attribute: [ 'href', 'title' ] } );
+
+			expect( consumed ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'href' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { attribute: 'title' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { attribute: 'name' } ) ).to.be.true;
+		} );
+
+		it( 'should consume multiple styles', () => {
+			viewConsumable.add( el, { style: [ 'color', 'top', 'position' ] } );
+
+			const consumed = viewConsumable.consume( el, { style: [ 'color', 'position' ] } );
+
+			expect( consumed ).to.be.true;
+			expect( viewConsumable.test( el, { style: 'color' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { style: 'position' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { style: 'top' } ) ).to.be.true;
+		} );
+
+		it( 'should consume multiple classes', () => {
+			viewConsumable.add( el, { class: [ 'foo', 'bar', 'baz' ] } );
+
+			const consumed = viewConsumable.consume( el, { class: [ 'bar', 'baz' ] } );
+
+			expect( consumed ).to.be.true;
+			expect( viewConsumable.test( el, { class: 'bar' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { class: 'baz' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { class: 'foo' } ) ).to.be.true;
+		} );
+
+		it( 'should consume only if all items can be consumed', () => {
+			viewConsumable.add( el, { style: [ 'position', 'color' ], attribute: [ 'href', 'title' ] } );
+
+			const consumed = viewConsumable.consume( el, { style: [ 'color', 'top' ] } );
+			expect( consumed ).to.be.false;
+			expect( viewConsumable.test( el, { style: 'color' } ) ).to.be.true;
+		} );
+
+		it( 'should consume all classes when class attribute is provided', () => {
+			expect( viewConsumable.consume( el, { attribute: 'class' } ) ).to.be.false;
+			viewConsumable.add( el, { class: [ 'foo', 'bar', 'baz' ] } );
+			expect( viewConsumable.consume( el, { attribute: 'class' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'class' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { class: 'foo' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { class: 'bar' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { class: 'baz' } ) ).to.be.false;
+		} );
+
+		it( 'should consume all styles when style attribute is provided', () => {
+			expect( viewConsumable.consume( el, { attribute: 'style' } ) ).to.be.false;
+			viewConsumable.add( el, { style: [ 'color', 'top', 'position' ] } );
+			expect( viewConsumable.consume( el, { attribute: 'style' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'style' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { style: 'color' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { style: 'top' } ) ).to.be.false;
+			expect( viewConsumable.test( el, { style: 'position' } ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'revert', () => {
+		it( 'should revert single element', () => {
+			viewConsumable.add( el, { name: true } );
+			viewConsumable.consume( el, { name: true } );
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.false;
+			viewConsumable.revert( el, { name: true } );
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.true;
+		} );
+
+		it( 'should revert text node', () => {
+			const text1 = new ViewText();
+			const text2 = new ViewText();
+
+			viewConsumable.add( text1 );
+			viewConsumable.consume( text1 );
+
+			viewConsumable.revert( text1 );
+			viewConsumable.revert( text2 );
+
+			expect( viewConsumable.test( text1 ) ).to.be.true;
+			expect( viewConsumable.test( text2 ) ).to.be.null;
+		} );
+
+		it( 'should revert document fragment', () => {
+			const fragment1 = new ViewDocumentFragment();
+			const fragment2 = new ViewDocumentFragment();
+
+			viewConsumable.add( fragment1 );
+			viewConsumable.consume( fragment1 );
+
+			viewConsumable.revert( fragment1 );
+			viewConsumable.revert( fragment2 );
+
+			expect( viewConsumable.test( fragment1 ) ).to.be.true;
+			expect( viewConsumable.test( fragment2 ) ).to.be.null;
+		} );
+
+		it( 'should not revert element that was never added', () => {
+			viewConsumable.revert( el, { name: true } );
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.null;
+		} );
+
+		it( 'should do nothing on not consumed element', () => {
+			viewConsumable.add( el, { name: true } );
+			viewConsumable.revert( el, { name: true } );
+			expect( viewConsumable.test( el, { name: true } ) ).to.be.true;
+		} );
+
+		it( 'should revert classes, attributes and styles', () => {
+			viewConsumable.add( el, { class: 'foobar', style: 'color', attribute: 'name' } );
+			viewConsumable.consume( el, { class: 'foobar', style: 'color', attribute: 'name' } );
+
+			viewConsumable.revert( el, { class: 'foobar' } );
+			viewConsumable.revert( el, { style: 'color' } );
+			viewConsumable.revert( el, { attribute: 'name' } );
+
+			expect( viewConsumable.test( el, { class: 'foobar', style: 'color', attribute: 'name' } ) ).to.be.true;
+		} );
+
+		it( 'should revert multiple classes, attributes and styles in one call #1', () => {
+			viewConsumable.add( el, {
+				class: 'foobar',
+				style: 'color',
+				attribute: 'name'
+			} );
+			viewConsumable.consume( el, { class: 'foobar', style: 'color', attribute: 'name' } );
+			viewConsumable.revert( el, { class: 'foobar', style: 'color', attribute: 'name' } );
+
+			expect( viewConsumable.test( el, { class: 'foobar', style: 'color', attribute: 'name' } ) ).to.be.true;
+		} );
+
+		it( 'should revert multiple classes, attributes and styles in one call #2', () => {
+			const consumables = {
+				class: [ 'foobar', 'baz' ],
+				style: [ 'color', 'position' ],
+				attribute: [ 'name', 'href' ]
+			};
+
+			viewConsumable.add( el, consumables );
+			viewConsumable.consume( el, consumables );
+			viewConsumable.revert( el, consumables );
+
+			expect( viewConsumable.test( el, consumables ) ).to.be.true;
+		} );
+
+		it( 'should revert only items that were previously added', () => {
+			viewConsumable.add( el, { class: 'foobar' } );
+			viewConsumable.consume( el, { class: 'foobar' } );
+			viewConsumable.revert( el, { class: 'foobar', attribute: 'name' } );
+
+			expect( viewConsumable.test( el, { class: 'foobar' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { attribute: 'name' } ) ).to.be.null;
+		} );
+
+		it( 'should revert all classes when class attribute is provided', () => {
+			viewConsumable.add( el, { class: [ 'foo', 'bar', 'baz' ] } );
+			expect( viewConsumable.consume( el, { class: [ 'foo', 'bar', 'baz' ] } ) ).to.be.true;
+			viewConsumable.revert( el, { attribute: 'class' } );
+
+			expect( viewConsumable.test( el, { class: 'foo' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { class: 'bar' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { class: 'baz' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { class: 'qux' } ) ).to.be.null;
+		} );
+
+		it( 'should revert all styles when style attribute is provided', () => {
+			viewConsumable.add( el, { style: [ 'color', 'top' ] } );
+			expect( viewConsumable.consume( el, { style: [ 'color', 'top' ] } ) ).to.be.true;
+			viewConsumable.revert( el, { attribute: 'style' } );
+
+			expect( viewConsumable.test( el, { style: 'color' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { style: 'top' } ) ).to.be.true;
+			expect( viewConsumable.test( el, { style: 'qux' } ) ).to.be.null;
+		} );
+	} );
+
+	describe( 'consumablesFromElement', () => {
+		it( 'should create consumable object from element', () => {
+			const consumables = ViewConsumable.consumablesFromElement( el );
+
+			expect( consumables ).to.be.an( 'object' );
+			expect( consumables.name ).to.be.true;
+			expect( consumables.attribute ).to.be.an( 'array' );
+			expect( consumables.attribute.length ).to.equal( 0 );
+			expect( consumables.class ).to.be.an( 'array' );
+			expect( consumables.class.length ).to.equal( 0 );
+			expect( consumables.style ).to.be.an( 'array' );
+			expect( consumables.style.length ).to.equal( 0 );
+		} );
+
+		it( 'should add all attributes', () => {
+			el.setAttribute( 'title', 'foobar' );
+			el.setAttribute( 'href', 'https://ckeditor.com' );
+
+			const consumables = ViewConsumable.consumablesFromElement( el );
+			expect( consumables.attribute.length ).to.equal( 2 );
+			expect( consumables.attribute.indexOf( 'title' ) > -1 ).to.be.true;
+			expect( consumables.attribute.indexOf( 'href' ) > -1 ).to.be.true;
+			expect( consumables.class.length ).to.equal( 0 );
+			expect( consumables.style.length ).to.equal( 0 );
+			expect( consumables.name ).to.be.true;
+		} );
+
+		it( 'should add all classes', () => {
+			el.addClass( 'foo', 'bar', 'baz' );
+
+			const consumables = ViewConsumable.consumablesFromElement( el );
+			expect( consumables.class.length ).to.equal( 3 );
+			expect( consumables.class.indexOf( 'foo' ) > -1 ).to.be.true;
+			expect( consumables.class.indexOf( 'bar' ) > -1 ).to.be.true;
+			expect( consumables.class.indexOf( 'baz' ) > -1 ).to.be.true;
+			expect( consumables.attribute.length ).to.equal( 0 );
+			expect( consumables.style.length ).to.equal( 0 );
+			expect( consumables.name ).to.be.true;
+		} );
+
+		it( 'should add all styles', () => {
+			el.setStyle( {
+				color: 'red',
+				position: 'absolute'
+			} );
+
+			const consumables = ViewConsumable.consumablesFromElement( el );
+			expect( consumables.style.length ).to.equal( 2 );
+			expect( consumables.style.indexOf( 'color' ) > -1 ).to.be.true;
+			expect( consumables.style.indexOf( 'position' ) > -1 ).to.be.true;
+			expect( consumables.attribute.length ).to.equal( 0 );
+			expect( consumables.class.length ).to.equal( 0 );
+			expect( consumables.name ).to.be.true;
+		} );
+	} );
+
+	describe( 'createFromElement', () => {
+		it( 'should return new ViewConsumable instance', () => {
+			const newConsumable = ViewConsumable.createFromElement( el );
+
+			expect( newConsumable ).to.be.instanceof( ViewConsumable );
+			expect( newConsumable.test( el, { name: true } ) ).to.be.true;
+		} );
+
+		it( 'should return new ViewConsumable instance from document fragment', () => {
+			const fragment = new ViewDocumentFragment();
+			const newConsumable = ViewConsumable.createFromElement( fragment );
+
+			expect( newConsumable ).to.be.instanceof( ViewConsumable );
+			expect( newConsumable.test( fragment ) ).to.be.true;
+		} );
+
+		it( 'should add all child elements', () => {
+			const text1 = new ViewText( 'foo' );
+			const text2 = new ViewText( 'bar' );
+			const child1 = new ViewElement( 'p', { 'title': 'baz' }, [ text1 ] );
+			const child2 = new ViewElement( 'p' );
+			const child3 = new ViewElement( 'p', { 'style': 'top:10px;', 'class': 'qux bar' }, [ text2, child2 ] );
+			el.appendChildren( [ child1, child3 ] );
+
+			const newConsumable = ViewConsumable.createFromElement( el );
+
+			expect( newConsumable.test( el, { name: true } ) ).to.be.true;
+			expect( newConsumable.test( text1 ) ).to.be.true;
+			expect( newConsumable.test( text2 ) ).to.be.true;
+			expect( newConsumable.test( child1, { name: true, attribute: 'title' } ) ).to.be.true;
+			expect( newConsumable.test( child2, { name: true } ) ).to.be.true;
+			expect( newConsumable.test( child3, { name: true, style: 'top', class: [ 'qux', 'bar' ] } ) ).to.be.true;
+		} );
+	} );
+} );

+ 29 - 0
packages/ckeditor5-engine/tests/treeview/element.js

@@ -614,6 +614,19 @@ describe( 'Element', () => {
 				expect( el.hasClass( 'three', 'one', 'two', 'zero' ) ).to.be.false;
 			} );
 		} );
+
+		describe( 'getClassNames', () => {
+			it( 'should return iterator with all class names', () => {
+				const names = [ 'one', 'two', 'three' ];
+				el.addClass( ...names );
+				const iterator = el.getClassNames();
+				let i = 0;
+
+				for ( let name of iterator ) {
+					expect( name ).to.equal( names[ i++ ] );
+				}
+			} );
+		} );
 	} );
 
 	describe( 'styles manipulation methods', () => {
@@ -667,6 +680,22 @@ describe( 'Element', () => {
 			} );
 		} );
 
+		describe( 'getStyleNames', () => {
+			it( 'should return iterator with all style names', () => {
+				const names = [ 'color', 'position' ];
+				el.setStyle( {
+					color: 'red',
+					position: 'absolute'
+				} );
+				const iterator = el.getStyleNames();
+				let i = 0;
+
+				for ( let name of iterator ) {
+					expect( name ).to.equal( names[ i++ ] );
+				}
+			} );
+		} );
+
 		describe( 'hasStyle', () => {
 			it( 'should check if element has a style', () => {
 				el.setStyle( 'padding-top', '10px' );