瀏覽代碼

Added: model <-> view converters builders.

Szymon Cofalik 9 年之前
父節點
當前提交
1a88effa3d

+ 266 - 0
packages/ckeditor5-engine/src/treecontroller/model-converter-builder.js

@@ -0,0 +1,266 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import {
+	insertElement,
+	setAttribute,
+	removeAttribute,
+	wrap,
+	unwrap
+} from '/ckeditor5/engine/treecontroller/model-to-view-converters.js';
+
+import ViewAttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
+import ViewContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
+
+/**
+ * Provides chainable, high-level API to easily build basic model-to-view converters that are appended to given
+ * dispatchers. In many cases, this is the API that should be used to specify how abstract model elements and
+ * attributes should be represented in the view (and then later in DOM). Instances of this class are created by
+ * {@link engine.treeController.BuildModelConverterFor}.
+ *
+ * If you need more complex converters, see {@link engine.treeController.ModelConversionDispatcher},
+ * {@link engine.treeController.modelToView}, {@link engine.treeController.ModelConsumable}, {@link engine.treeController.Mapper}.
+ *
+ * Using this API it is possible to create three kinds of converters:
+ *
+ * 1. Model element to view element converter. This is a converter that takes the model element and represents it
+ * in the view.
+ *
+ *		BuildModelConverterFor( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
+ *		BuildModelConverterFor( dispatcher ).fromElement( 'image' ).toElement( 'img' );
+ *
+ * 2. Model attribute to view attribute converter. This is a converter that operates on model element attributes
+ * and converts them to view element attributes. It is suitable for elements like `image` (`src`, `title` attributes).
+ *
+ *		BuildModelConverterFor( dispatcher ).fromElement( 'image' ).toElement( 'img' );
+ *		BuildModelConverterFor( dispatcher ).fromAttribute( 'src' ).toAttribute();
+ *
+ * 3. Model attribute to view element converter. This is a converter that takes model attributes and represents them
+ * as view elements. Those view elements are wrapping view elements are node that correspond to model elements and
+ * nodes which had converter attribute. It is suitable for attributes like `bold`, where `bold` attribute set on model
+ * text nodes is converter to `strong` view element.
+ *
+ *		BuildModelConverterFor( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
+ *
+ * It is possible to provide various different parameters for {@link engine.treeController.ModelConverterBuilder#toElement}
+ * and {@link engine.treeController.ModelConverterBuilder#toAttribute} methods. See their descriptions to learn more.
+ *
+ * It is also possible to {@link engine.treeController.ModelConverterBuilder#withPriority change default priority}
+ * of created converters to decide which converter should be fired earlier and which later. This is useful if you provide
+ * a general converter but want to provide different converter for a specific-case (i.e. given model element is converted
+ * always to given view element, but if it has given attribute it is converter to other view element). For this,
+ * use {@link engine.treeController.ModelConverterBuilder#withPriority withPriority} right after `from...` method.
+ *
+ * Note that `to...` methods are "terminators", which means that should be the last one used in building converter.
+ *
+ * You can use {@link engine.treeController.ViewConverterBuilder} to create "opposite" converters - from view to model.
+ *
+ * @memberOf engine.treeController
+ */
+class ModelConverterBuilder {
+	/**
+	 * Creates `ModelConverterBuilder` with given `dispatchers` registered to it.
+	 *
+	 * @param {Array.<engine.treeController.ModelConversionDispatcher>} dispatchers Dispatchers to which converters will
+	 * be attached.
+	 */
+	constructor( dispatchers ) {
+		/**
+		 * Dispatchers to which converters will be attached.
+		 *
+		 * @type {Array.<engine.treeController.ModelConversionDispatcher>}
+		 * @private
+		 */
+		this._dispatchers = dispatchers;
+
+		/**
+		 * Contains data about registered "from" query.
+		 *
+		 * @type {Object}
+		 * @private
+		 */
+		this._from = null;
+	}
+
+	/**
+	 * Registers what model element should be converted.
+	 *
+	 * @chainable
+	 * @param {String} elementName Name of element to convert.
+	 * @returns {engine.treeController.ModelConverterBuilder}
+	 */
+	fromElement( elementName ) {
+		this._from = {
+			type: 'element',
+			name: elementName,
+			priority: null
+		};
+
+		return this;
+	}
+
+	/**
+	 * Registers what model attribute should be converted.
+	 *
+	 * @chainable
+	 * @param {String} key Key of attribute to convert.
+	 * @returns {engine.treeController.ModelConverterBuilder}
+	 */
+	fromAttribute( key ) {
+		this._from = {
+			type: 'attribute',
+			key: key,
+			priority: null
+		};
+
+		return this;
+	}
+
+	/**
+	 * Changes default priority for built converter.
+	 *
+	 * @param {Number} priority Converter priority.
+	 * @returns {engine.treeController.ModelConverterBuilder}
+	 */
+	withPriority( priority ) {
+		this._from.priority = priority;
+
+		return this;
+	}
+
+	/**
+	 * Registers what view element will be created by converter.
+	 *
+	 * Method accepts various ways of providing how the view element will be created. You can pass view element name as
+	 * `string`, view element instance which will be cloned and used, or creator function which returns view element that
+	 * will be used. Keep in mind that when you view element instance or creator function, it has to be/return a
+	 * proper type of view element: {@link engine.treeView.ViewContainerElement ViewContainerElement} if you convert
+	 * from element or {@link engine.treeView.ViewAttributeElement ViewAttributeElement} if you convert from attribute.
+	 *
+	 *		BuildModelConverterFor( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
+	 *
+	 *		BuildModelConverterFor( dispatcher ).fromElement( 'image' ).toElement( new ViewContainerElement( 'img' ) );
+	 *
+	 *		BuildModelConverterFor( dispatcher )
+	 *			.fromElement( 'header' )
+	 *			.toElement( ( data ) => new ViewContainerElement( 'h' + data.item.getAttribute( 'level' ) ) );
+	 *
+	 *		BuildModelConverterFor( dispatcher ).fromAttribute( 'bold' ).toElement( new ViewAttributeElement( 'strong' ) );
+	 *
+	 * Creator function will be passed different values depending whether conversion is from element or from attribute:
+	 *
+	 * * from element: dispatcher's {@link engine.treeController.ModelConversionDispatcher#event:insert insert event} parameters
+	 * will be passed
+	 * * from attribute: first parameter is attribute value, then the rest of parameters are dispatcher's
+	 * {@link engine.treeController.ModelConversionDispatcher#event:changeAttribute changeAttribute event} parameters.
+	 *
+	 * This method creates the converter and adds it as a callback to a proper
+	 * {@link engine.treeController.ModelConversionDispatcher conversion dispatcher} event.
+	 *
+	 * @param {String|engine.treeView.ViewElement|Function} element Element created by converter.
+	 */
+	toElement( element ) {
+		const priority = this._from.priority === null ? 10 : this._from.priority;
+
+		for ( let dispatcher of this._dispatchers ) {
+			if ( this._from.type == 'element' ) {
+				// From model element to view element -> insert element.
+				element = typeof element == 'string' ? new ViewContainerElement( element ) : element;
+
+				dispatcher.on( 'insert:' + this._from.name, insertElement( element ), null, priority );
+			} else {
+				// From model attribute to view element -> wrap and unwrap.
+				element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
+
+				dispatcher.on( 'addAttribute:' + this._from.key, wrap( element ), null, priority );
+				dispatcher.on( 'changeAttribute:' + this._from.key, wrap( element ), null, priority );
+				dispatcher.on( 'removeAttribute:' + this._from.key, unwrap( element ), null, priority );
+			}
+		}
+	}
+
+	/**
+	 * Registers what view attribute will be created by converter. Keep in mind, that only model attribute to
+	 * view attribute conversion is supported.
+	 *
+	 * Method accepts various ways of providing how the view element will be created:
+	 *
+	 * * for no passed parameter, attribute key and value will be converted 1-to-1 to view attribute,
+	 * * if you pass one `string`, it will be used as new attribute key while attribute value will be copied,
+	 * * if you pass two `string`s, first one will be used as new attribute key and second one as new attribute value,
+	 * * if you pass a function, it is expected to return an object with `key` and `value` properties representing attribute key and value.
+	 * This function will be passed model attribute value and model attribute key as first two parameters and then
+	 * all dispatcher's {engine.treeController.ModelConversionDispatcher#event:changeAttribute changeAttribute event} parameters.
+	 *
+	 *		BuildModelConverterFor( dispatcher ).fromAttribute( 'class' ).toAttribute( '' );
+	 *
+	 *		BuildModelConverterFor( dispatcher ).fromAttribute( 'linkTitle' ).toAttribute( 'title' );
+	 *
+	 *		BuildModelConverterFor( dispatcher ).fromAttribute( 'highlighted' ).toAttribute( 'style', 'background:yellow' );
+	 *
+	 *		BuildModelConverterFor( dispatcher )
+	 *			.fromAttribute( 'theme' )
+	 *			.toAttribute( ( value ) => ( { key: 'class', value: value + '-theme' } ) );
+	 *
+	 * This method creates the converter and adds it as a callback to a proper
+	 * {@link engine.treeController.ModelConversionDispatcher conversion dispatcher} event.
+	 *
+	 * @param {String|Function} [keyOrCreator] Attribute key or a creator function.
+	 * @param {*} [value] Attribute value.
+	 */
+	toAttribute( keyOrCreator, value ) {
+		if ( this._from.type == 'element' ) {
+			// Converting from model element to view attribute is unsupported.
+			return;
+		}
+
+		let attributeCreator;
+
+		if ( !keyOrCreator ) {
+			// If `keyOrCreator` is not set, we assume default behavior which is 1:1 attribute re-write.
+			// This is also a default behavior for `setAttribute` converter when no attribute creator is passed.
+			attributeCreator = undefined;
+		} else if ( typeof keyOrCreator == 'string' ) {
+			// `keyOrCreator` is an attribute key.
+
+			if ( value ) {
+				// If value is set, create "dumb" creator that always returns the same object.
+				attributeCreator = function() {
+					return { key: keyOrCreator, value: value };
+				};
+			} else {
+				// If value is not set, take it from the passed parameter.
+				attributeCreator = function( value ) {
+					return { key: keyOrCreator, value: value };
+				};
+			}
+		} else {
+			// `keyOrCreator` is an attribute creator function.
+			attributeCreator = keyOrCreator;
+		}
+
+		for ( let dispatcher of this._dispatchers ) {
+			dispatcher.on( 'addAttribute:' + this._from.key, setAttribute( attributeCreator ), null, this._from.priority || 10 );
+			dispatcher.on( 'changeAttribute:' + this._from.key, setAttribute( attributeCreator ), null, this._from.priority || 10 );
+			dispatcher.on( 'removeAttribute:' + this._from.key, removeAttribute( attributeCreator ), null, this._from.priority || 10 );
+		}
+	}
+}
+
+/**
+ * Entry point for model-to-view converters builder. This chainable API makes it easy to create basic, most common
+ * model-to-view converters and attach them to provided dispatchers. The method returns an instance of
+ * {@link engine.treeController.ModelConverterBuilder}.
+ *
+ * @external engine.treeController.BuildModelConverterFor
+ * @memberOf engine.treeController
+ * @param {...engine.treeController.ModelConversionDispatcher} dispatchers One or more dispatchers which
+ * the built converter will be attached to.
+ */
+export default function BuildModelConverterFor( ...dispatchers ) {
+	return new ModelConverterBuilder( dispatchers );
+}

+ 404 - 0
packages/ckeditor5-engine/src/treecontroller/view-converter-builder.js

@@ -0,0 +1,404 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Matcher from '../treeview/matcher.js';
+import ModelElement from '../treemodel/element.js';
+import utils from '../../utils/utils';
+
+/**
+ * Provides chainable, high-level API to easily build basic view-to-model converters that are appended to given
+ * dispatchers. View-to-model converters are used when external data is added to the editor, i.e. when a user pastes
+ * HTML content to the editor. Then, converters are used to translate this structure, possibly removing unknown/incorrect
+ * nodes, and add it to the model. Also multiple, different elements might be translated into the same thing in the
+ * model, i.e. `<b>` and `<strong>` elements might be converted to `bold` attribute (even though `bold` attribute will
+ * be then converted only to `<strong>` tag). Instances of this class are created by {@link engine.treeController.BuildViewConverterFor}.
+ *
+ * If you need more complex converters, see {@link engine.treeController.ViewConversionDispatcher},
+ * {@link engine.treeController.viewToModel}, {@link engine.treeController.ViewConsumable}.
+ *
+ * Using this API it is possible to create various kind of converters:
+ *
+ * 1. View element to model element:
+ *
+ *		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+ *
+ * 2. View element to model attribute:
+ *
+ *		BuildViewConverterFor( dispatcher ).fromElement( 'b' ).fromElement( 'strong' ).toAttributes( { bold: true } );
+ *
+ * 3. View attribute to model attribute:
+ *
+ *		BuildViewConverterFor( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttributes( { bold: true } );
+ *		BuildViewConverterFor( dispatcher )
+ *			.fromAttribute( 'class' )
+ *			.toAttributes( ( viewElement ) => ( { class: viewElement.getAttribute( 'class' ) } ) );
+ *
+ * 4. View elements and attributes to model attribute:
+ *
+ *		BuildViewConverterFor( dispatcher )
+ *			.fromElement( 'b' ).fromElement( 'strong' ).fromAttribute( 'style', { 'font-weight': 'bold' } )
+ *			.toAttributes( { bold: true } );
+ *
+ * 5. View {@link engine.treeView.Matcher view element matcher instance} or {@link engine.treeView.Matcher#add matcher pattern}
+ * to model element or attribute:
+ *
+ *		const matcher = new ViewMatcher();
+ *		matcher.add( 'div', { class: 'quote' } );
+ *		BuildViewConverterFor( dispatcher ).from( matcher ).toElement( 'quote' );
+ *
+ *		BuildViewConverterFor( dispatcher ).from( { name: 'span', class: 'bold' } ).toAttributes( { bold: true } );
+ *
+ * Note, that converters built using `ViewConverterBuilder` automatically check {@link engine.treeModel.Schema schema}
+ * if created model structure is valid. If given conversion would be invalid according to schema, it is ignored.
+ *
+ * It is possible to provide creator functions as parameters for {@link engine.treeController.ViewConverterBuilder#toElement}
+ * and {@link engine.treeController.ViewConverterBuilder#toAttributes} methods. See their descriptions to learn more.
+ *
+ * By default, converter will {@link engine.treeController.ViewConsumable#consume consume} every value specified in
+ * given `from...` query, i.e. `.from( { name: 'span', class: 'bold' } )` will make converter consume both `span` name
+ * and `bold` class. It is possible to change this behavior using {@link engine.treeController.ViewConverterBuilder#consuming consuming}
+ * modifier. The modifier alters the last `fromXXX` query used before it. To learn more about consuming values,
+ * see {@link engine.treeController.ViewConsumable}.
+ *
+ * It is also possible to {@link engine.treeController.ViewConverterBuilder#withPriority change default priority}
+ * of created converters to decide which converter should be fired earlier and which later. This is useful if you provide
+ * a general converter but want to provide different converter for a specific-case (i.e. given view element is converted
+ * always to given model element, but if it has given class it is converter to other model element). For this,
+ * use {@link engine.treeController.ViewConverterBuilder#withPriority withPriority} modifier. The modifier alters
+ * the last `from...` query used before it.
+ *
+ * Note that `to...` methods are "terminators", which means that should be the last one used in building converter.
+ *
+ * You can use {@link engine.treeController.ModelConverterBuilder} to create "opposite" converters - from model to view.
+ *
+ * @memberOf engine.treeController
+ */
+class ViewConverterBuilder {
+	/**
+	 * Creates `ViewConverterBuilder` with given `dispatchers` registered to it.
+	 *
+	 * @param {Array.<engine.treeController.ViewConversionDispatcher>} dispatchers Dispatchers to which converters will
+	 * be attached.
+	 */
+	constructor( dispatchers ) {
+		/**
+		 * Dispatchers to which converters will be attached.
+		 *
+		 * @type {Array.<engine.treeController.ViewConversionDispatcher>}
+		 * @private
+		 */
+		this._dispatchers = dispatchers;
+
+		/**
+		 * Stores "from" queries.
+		 *
+		 * @type {Array}
+		 * @private
+		 */
+		this._from = [];
+	}
+
+	/**
+	 * Registers what view element should be converted.
+	 *
+	 *		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+	 *
+	 * @param {String} elementName View element name.
+	 * @returns {engine.treeController.ViewConverterBuilder}
+	 */
+	fromElement( elementName ) {
+		return this.from( { name: elementName } );
+	}
+
+	/**
+	 * Registers what view attribute should be converted.
+	 *
+	 *		BuildViewConverterFor( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttributes( { bold: true } );
+	 *
+	 * @param {String|RegExp} key View attribute key.
+	 * @param {String|RegExp} [value] View attribute value.
+	 * @returns {engine.treeController.ViewConverterBuilder}
+	 */
+	fromAttribute( key, value = /.*/ ) {
+		let pattern = {};
+		pattern[ key ] = value;
+
+		return this.from( pattern );
+	}
+
+	/**
+	 * Registers what view pattern should be converted. The method accepts either {@link engine.treeView.Matcher view matcher}
+	 * or view matcher pattern.
+	 *
+	 *		const matcher = new ViewMatcher();
+	 *		matcher.add( 'div', { class: 'quote' } );
+	 *		BuildViewConverterFor( dispatcher ).from( matcher ).toElement( 'quote' );
+	 *
+	 *		BuildViewConverterFor( dispatcher ).from( { name: 'span', class: 'bold' } ).toAttributes( { bold: true } );
+	 *
+	 * @param {Object|engine.treeView.Matcher} matcher View matcher or view matcher pattern.
+	 * @returns {engine.treeController.ViewConverterBuilder}
+	 */
+	from( matcher ) {
+		if ( !( matcher instanceof Matcher ) ) {
+			matcher = new Matcher( matcher );
+		}
+
+		this._from.push( {
+			matcher: matcher,
+			consume: false,
+			priority: null
+		} );
+
+		return this;
+	}
+
+	/**
+	 * Modifies which consumable values will be {@link engine.treeController.ViewConsumable#consume consumed} by built converter.
+	 * It modifies the last `from...` query. Can be used after each `from...` query in given chain. Useful for providing
+	 * more specific matches.
+	 *
+	 *		// This converter will only handle class bold conversion (to proper attribute) but span element
+	 *		// conversion will have to be done in separate converter.
+	 *		// Without consuming modifier, the converter would consume both class and name, so a converter for
+	 *		// span element would not be fired.
+	 *		BuildViewConverterFor( dispatcher )
+	 *			.from( { name: 'span', class: 'bold' } ).consuming( { class: 'bold' } )
+	 *			.toAttribute( { bold: true } );
+	 *
+	 *		BuildViewConverterFor( dispatcher )
+	 *			.fromElement( 'img' ).consuming( { name: true, attributes: [ 'src', 'title' ] } )
+	 *			.toElement( ( viewElement ) => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ),
+	 *																		title: viewElement.getAttribute( 'title' ) } );
+	 *
+	 * **Note:** All and only values from passed object has to be consumable on converted view element. This means that
+	 * using `consuming` method, you can either make looser conversion conditions (like in first example) or tighter
+	 * conversion conditions (like in second example). Nevertheless, the view element has to match `from...` query clause.
+	 *
+	 * @see engine.treeController.ViewConsumable
+	 * @param {Object} consume Values to consume.
+	 * @returns {engine.treeController.ViewConverterBuilder}
+	 */
+	consuming( consume ) {
+		let lastFrom = this._from[ this._from.length - 1 ];
+		lastFrom.consume = consume;
+
+		return this;
+	}
+
+	/**
+	 * Changes default priority for built converter. It modifies the last `from...` query. Can be used after each
+	 * `from...` query in given chain. Useful for overwriting converters.
+	 *
+	 *		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+	 *		// Register converter with proper priority, otherwise "p" element would get consumed by first
+	 *		// converter and the second converter would not be fired.
+	 *		BuildViewConverterFor( dispatcher )
+	 *			.from( { name: 'p', class: 'custom' } ).withPriority( 9 )
+	 *			.toElement( 'customParagraph' );
+	 *
+	 * **Note:** `ViewConverterBuilder` takes care so all `toElement` conversions takes place before all `toAttributes`
+	 * conversions. This is done by setting default `toElement` priority to `10` and `toAttributes` priority to `1000`.
+	 * It is recommended to set converter priority for `toElement` conversions below `500` and `toAttributes` priority
+	 * above `500`. It is important that model elements are created before attributes, otherwise attributes would
+	 * not be applied or other errors may occur.
+	 *
+	 * @param {Number} priority Converter priority.
+	 * @returns {engine.treeController.ViewConverterBuilder}
+	 */
+	withPriority( priority ) {
+		let lastFrom = this._from[ this._from.length - 1 ];
+		lastFrom.priority = priority;
+
+		return this;
+	}
+
+	/**
+	 * Registers what model element will be created by converter.
+	 *
+	 * Method accepts two ways of providing what kind of model element will be created. You can pass model element
+	 * name as a `string` or a function that will return model element instance. If you provide creator function,
+	 * it will be passed converted view element as first and only parameter.
+	 *
+	 *		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+	 *		BuildViewConverterFor( dispatcher )
+	 *			.fromElement( 'img' )
+	 *			.toElement( ( viewElement ) => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } );
+	 *
+	 * @param {String|Function} element Model element name or model element creator function.
+	 */
+	toElement( element ) {
+		const eventCallbackGen = function( from ) {
+			return ( evt, data, consumable, conversionApi ) => {
+				// There is one callback for all patterns in the matcher.
+				// This will be usually just one pattern but we support matchers with many patterns too.
+				let matchAll = from.matcher.matchAll( data.input );
+
+				// If there is no match, this callback should not do anything.
+				if ( !matchAll ) {
+					return;
+				}
+
+				// Now, for every match between matcher and actual element, we will try to consume the match.
+				for ( let match of matchAll ) {
+					// Create model element basing on creator function or element name.
+					const modelElement = element instanceof Function ? element( data.input ) : new ModelElement( element );
+
+					// Check whether generated structure is okay with `Schema`.
+					// TODO: Make it more sane after .getAttributeKeys() is available for ModelElement.
+					const keys = Array.from( modelElement.getAttributes() ).map( ( attribute ) => attribute[ 0 ] );
+
+					if ( !conversionApi.schema.check( { name: modelElement.name, attributes: keys, inside: data.context } ) ) {
+						continue;
+					}
+
+					// Try to consume appropriate values from consumable values list.
+					if ( !consumable.consume( data.input, from.consume || match.match ) ) {
+						continue;
+					}
+
+					// If everything is fine, we are ready to start the conversion.
+					// Add newly created `modelElement` to the parents stack.
+					data.context.push( modelElement );
+
+					// Convert children of converted view element and append them to `modelElement`.
+					modelElement.appendChildren( conversionApi.convertChildren( data.input, consumable, data ) );
+
+					// Remove created `modelElement` from the parents stack.
+					data.context.pop();
+
+					// Add `modelElement` as a result.
+					data.output = modelElement;
+
+					// Prevent multiple conversion if there are other correct matches.
+					break;
+				}
+			};
+		};
+
+		this._setCallback( eventCallbackGen, 10 );
+	}
+
+	/**
+	 * Registers what model attribute will be created by converter.
+	 *
+	 * Method accepts two ways of providing what kind of model attribute will be created. You can pass `object` which
+	 * keys will be translated to model attributes keys and values to model attributes values or you can pass a function which
+	 * will return such object. If you provide creator function, it will be passed converted view element as first and only parameter.
+	 *
+	 *		BuildViewConverterFor( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttributes( { bold: true } );
+	 *		BuildViewConverterFor( dispatcher )
+	 *			.fromAttribute( 'class' )
+	 *			.toAttributes( ( viewElement ) => ( { class: viewElement.getAttribute( 'class' ) } ) );
+	 *
+	 * @param {Object|Function} attributes Object with model attributes or creator function.
+	 */
+	toAttributes( attributes ) {
+		const eventCallbackGen = function( from ) {
+			return ( evt, data, consumable, conversionApi ) => {
+				// There is one callback for all patterns in the matcher.
+				// This will be usually just one pattern but we support matchers with many patterns too.
+				let matchAll = from.matcher.matchAll( data.input );
+
+				// If there is no match, this callback should not do anything.
+				if ( !matchAll ) {
+					return;
+				}
+
+				// Now, for every match between matcher and actual element, we will try to consume the match.
+				for ( let match of matchAll ) {
+					// Try to consume appropriate values from consumable values list.
+					if ( !consumable.consume( data.input, from.consume || match.match ) ) {
+						continue;
+					}
+
+					// Since we are converting to attribute we need an output on which we will set the attribute.
+					// If the output is not created yet, we will create it.
+					if ( !data.output ) {
+						data.output = conversionApi.convertChildren( data.input, consumable, data );
+					}
+
+					// Use attributes object creator function, if provided.
+					const modelAttributes = attributes instanceof Function ? attributes( data.input ) : attributes;
+
+					// Set attributes on current `output`. `Schema` is checked inside this helper function.
+					setAttributesOn( data.output, modelAttributes, data, conversionApi );
+
+					// Prevent multiple conversion if there are other correct matches.
+					break;
+				}
+			};
+		};
+
+		this._setCallback( eventCallbackGen, 1000 );
+	}
+
+	/**
+	 * Helper function that uses given callback generator to created callback function and sets it on registered dispatchers.
+	 *
+	 * @param eventCallbackGen
+	 * @param defaultPriority
+	 * @private
+	 */
+	_setCallback( eventCallbackGen, defaultPriority ) {
+		// We will add separate event callback for each registered `from` entry.
+		for ( let from of this._from ) {
+			// We have to figure out event name basing on matcher's patterns.
+			// If there is exactly one pattern and it has `name` property we will used that name.
+			const matcherElementName = from.matcher.getElementName();
+			const eventName = matcherElementName ? 'element:' + matcherElementName : 'element';
+			const eventCallback = eventCallbackGen( from );
+
+			const priority = from.priority === null ? defaultPriority : from.priority;
+
+			// Add event to each registered dispatcher.
+			for ( let dispatcher of this._dispatchers ) {
+				dispatcher.on( eventName, eventCallback, null, priority );
+			}
+		}
+	}
+}
+
+// Helper function that sets given attributes on given `engine.treeModel.Item` or `engine.treeModel.DocumentFragment`.
+function setAttributesOn( toChange, attributes, data, conversionApi ) {
+	if ( utils.isIterable( toChange ) ) {
+		for ( let node of toChange ) {
+			setAttributesOn( node, attributes, data, conversionApi );
+		}
+
+		return;
+	}
+
+	// TODO: Make it more sane after .getAttributeKeys() is available for ModelElement.
+	const keys = Array.from( toChange.getAttributes() ).map( ( attribute ) => attribute[ 0 ] ).concat( Object.keys( attributes ) );
+
+	const schemaQuery = {
+		name: toChange.name || '$text',
+		attributes: keys,
+		inside: data.context
+	};
+
+	if ( conversionApi.schema.check( schemaQuery ) ) {
+		for ( let key in attributes ) {
+			toChange.setAttribute( key, attributes[ key ] );
+		}
+	}
+}
+
+/**
+ * Entry point for view-to-model converters builder. This chainable API makes it easy to create basic, most common
+ * view-to-model converters and attach them to provided dispatchers. The method returns an instance of
+ * {@link engine.treeController.ViewConverterBuilder}.
+ *
+ * @external engine.treeController.BuildViewConverterFor
+ * @memberOf engine.treeController
+ * @param {...engine.treeController.ViewConversionDispatcher} dispatchers One or more dispatchers to which
+ * the built converter will be attached.
+ */
+export default function BuildViewConverterFor( ...dispatchers ) {
+	return new ViewConverterBuilder( dispatchers );
+}

+ 309 - 0
packages/ckeditor5-engine/tests/treecontroller/model-converter-builder.js

@@ -0,0 +1,309 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treecontroller */
+
+'use strict';
+
+import BuildModelConverterFor from '/ckeditor5/engine/treecontroller/model-converter-builder.js';
+
+import ModelDocument from '/ckeditor5/engine/treemodel/document.js';
+import ModelElement from '/ckeditor5/engine/treemodel/element.js';
+import ModelText from '/ckeditor5/engine/treemodel/text.js';
+import ModelRange from '/ckeditor5/engine/treemodel/range.js';
+
+import ViewElement from '/ckeditor5/engine/treeview/element.js';
+import ViewContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
+import ViewAttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
+import ViewText from '/ckeditor5/engine/treeview/text.js';
+import ViewWriter from '/ckeditor5/engine/treeview/writer.js';
+
+import Mapper from '/ckeditor5/engine/treecontroller/mapper.js';
+import ModelConversionDispatcher from '/ckeditor5/engine/treecontroller/modelconversiondispatcher.js';
+
+import {
+	insertText,
+	move,
+	remove
+} from '/ckeditor5/engine/treecontroller/model-to-view-converters.js';
+
+function viewAttributesToString( item ) {
+	let result = '';
+
+	for ( let key of item.getAttributeKeys() ) {
+		let value = item.getAttribute( key );
+
+		if ( value ) {
+			result += ' ' + key + '="' + value + '"';
+		}
+	}
+
+	return result;
+}
+
+function viewToString( item ) {
+	let result = '';
+
+	if ( item instanceof ViewText ) {
+		result = item.data;
+	} else {
+		// ViewElement or ViewDocumentFragment.
+		for ( let child of item.getChildren() ) {
+			result += viewToString( child );
+		}
+
+		if ( item instanceof ViewElement ) {
+			result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
+		}
+	}
+
+	return result;
+}
+
+describe( 'Model converter builder', () => {
+	let dispatcher, modelDoc, modelRoot, viewRoot, mapper, writer;
+
+	beforeEach( () => {
+		modelDoc = new ModelDocument();
+		modelRoot = modelDoc.createRoot( 'root', 'root' );
+
+		viewRoot = new ViewContainerElement( 'div' );
+
+		mapper = new Mapper();
+		mapper.bindElements( modelRoot, viewRoot );
+
+		writer = new ViewWriter();
+
+		dispatcher = new ModelConversionDispatcher( { writer, mapper } );
+
+		dispatcher.on( 'insert:$text', insertText() );
+		dispatcher.on( 'move', move() );
+		dispatcher.on( 'remove', remove() );
+	} );
+
+	describe( 'model element to view element conversion', () => {
+		it( 'using passed view element name', () => {
+			BuildModelConverterFor( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
+
+			let modelElement = new ModelElement( 'paragraph', null, 'foobar' );
+			modelRoot.appendChildren( modelElement );
+
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
+		} );
+
+		it( 'using passed view element', () => {
+			BuildModelConverterFor( dispatcher ).fromElement( 'image' ).toElement( new ViewContainerElement( 'img' ) );
+
+			let modelElement = new ModelElement( 'image' );
+			modelRoot.appendChildren( modelElement );
+
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><img></img></div>' );
+		} );
+
+		it( 'using passed creator function', () => {
+			BuildModelConverterFor( dispatcher )
+				.fromElement( 'header' )
+				.toElement( ( data ) => new ViewContainerElement( 'h' + data.item.getAttribute( 'level' ) ) );
+
+			let modelElement = new ModelElement( 'header', { level: 2 }, 'foobar' );
+			modelRoot.appendChildren( modelElement );
+
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><h2>foobar</h2></div>' );
+		} );
+	} );
+
+	describe( 'model attribute to view element conversion', () => {
+		beforeEach( () => {
+			BuildModelConverterFor( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
+		} );
+
+		it( 'using passed view element name', () => {
+			BuildModelConverterFor( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
+
+			let modelElement = new ModelText( 'foo', { bold: true } );
+			modelRoot.appendChildren( modelElement );
+
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><strong>foo</strong></div>' );
+
+			for ( let value of ModelRange.createFromElement( modelRoot ) ) {
+				value.item.removeAttribute( 'bold' );
+			}
+
+			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createFromElement( modelRoot ), 'bold', true, null );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
+		} );
+
+		it( 'using passed view element', () => {
+			BuildModelConverterFor( dispatcher ).fromAttribute( 'bold' ).toElement( new ViewAttributeElement( 'strong' ) );
+
+			let modelElement = new ModelText( 'foo', { bold: true } );
+			modelRoot.appendChildren( modelElement );
+
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><strong>foo</strong></div>' );
+
+			for ( let value of ModelRange.createFromElement( modelRoot ) ) {
+				value.item.removeAttribute( 'bold' );
+			}
+
+			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createFromElement( modelRoot ), 'bold', true, null );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
+		} );
+
+		it( 'using passed creator function', () => {
+			BuildModelConverterFor( dispatcher ).fromAttribute( 'italic' ).toElement( ( value ) => new ViewAttributeElement( value ) );
+
+			let modelElement = new ModelText( 'foo', { italic: 'em' } );
+			modelRoot.appendChildren( modelElement );
+
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );
+
+			for ( let value of ModelRange.createFromElement( modelRoot ) ) {
+				value.item.setAttribute( 'italic', 'i' );
+			}
+
+			dispatcher.convertAttribute( 'changeAttribute', ModelRange.createFromElement( modelRoot ), 'italic', 'em', 'i' );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><i>foo</i></div>' );
+
+			for ( let value of ModelRange.createFromElement( modelRoot ) ) {
+				value.item.removeAttribute();
+			}
+
+			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createFromElement( modelRoot ), 'italic', 'i', null );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
+		} );
+	} );
+
+	describe( 'model attribute to view attribute conversion', () => {
+		beforeEach( () => {
+			BuildModelConverterFor( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
+		} );
+
+		it( 'using default 1-to-1 conversion', () => {
+			BuildModelConverterFor( dispatcher ).fromAttribute( 'class' ).toAttribute();
+
+			let modelElement = new ModelElement( 'paragraph', { class: 'myClass' }, 'foobar' );
+			modelRoot.appendChildren( modelElement );
+
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="myClass">foobar</p></div>' );
+
+			modelElement.setAttribute( 'class', 'newClass' );
+			dispatcher.convertAttribute( 'changeAttribute', ModelRange.createOnElement( modelElement ), 'class', 'myClass', 'newClass' );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="newClass">foobar</p></div>' );
+
+			modelElement.removeAttribute( 'class' );
+			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createOnElement( modelElement ), 'class', 'newClass', null );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
+		} );
+
+		it( 'using passed attribute key', () => {
+			BuildModelConverterFor( dispatcher ).fromAttribute( 'theme' ).toAttribute( 'class' );
+
+			let modelElement = new ModelElement( 'paragraph', { theme: 'abc' }, 'foobar' );
+			modelRoot.appendChildren( modelElement );
+
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="abc">foobar</p></div>' );
+
+			modelElement.setAttribute( 'theme', 'xyz' );
+			dispatcher.convertAttribute( 'changeAttribute', ModelRange.createOnElement( modelElement ), 'theme', 'abc', 'xyz' );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="xyz">foobar</p></div>' );
+
+			modelElement.removeAttribute( 'theme' );
+			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createOnElement( modelElement ), 'theme', 'xyz', null );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
+		} );
+
+		it( 'using passed attribute key and value', () => {
+			BuildModelConverterFor( dispatcher ).fromAttribute( 'highlighted' ).toAttribute( 'style', 'background:yellow' );
+
+			let modelElement = new ModelElement( 'paragraph', { 'highlighted': true }, 'foobar' );
+			modelRoot.appendChildren( modelElement );
+
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p style="background:yellow;">foobar</p></div>' );
+
+			modelElement.removeAttribute( 'highlighted' );
+			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createOnElement( modelElement ), 'highlighted', true, null );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
+		} );
+
+		it( 'using passed attribute creator function', () => {
+			BuildModelConverterFor( dispatcher )
+				.fromAttribute( 'theme' )
+				.toAttribute( ( value ) => ( { key: 'class', value: value + '-theme' } ) );
+
+			let modelElement = new ModelElement( 'paragraph', { theme: 'nice' }, 'foobar' );
+			modelRoot.appendChildren( modelElement );
+
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="nice-theme">foobar</p></div>' );
+
+			modelElement.setAttribute( 'theme', 'good' );
+			dispatcher.convertAttribute( 'changeAttribute', ModelRange.createOnElement( modelElement ), 'theme', 'nice', 'good' );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="good-theme">foobar</p></div>' );
+
+			modelElement.removeAttribute( 'theme' );
+			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createOnElement( modelElement ), 'theme', 'good', null );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
+		} );
+	} );
+
+	describe( 'withPriority', () => {
+		it( 'should change default converters priority', () => {
+			BuildModelConverterFor( dispatcher ).fromElement( 'custom' ).toElement( 'custom' );
+			BuildModelConverterFor( dispatcher ).fromElement( 'custom' ).withPriority( 0 ).toElement( 'other' );
+
+			let modelElement = new ModelElement( 'custom', null, 'foobar' );
+			modelRoot.appendChildren( modelElement );
+
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><other>foobar</other></div>' );
+		} );
+	} );
+
+	it( 'should do nothing on model element to view attribute conversion', () => {
+		BuildModelConverterFor( dispatcher ).fromElement( 'div' ).toElement( 'div' );
+		// Should do nothing:
+		BuildModelConverterFor( dispatcher ).fromElement( 'paragraph' ).toAttribute( 'paragraph', true );
+		// If above would do something this one would not be fired:
+		BuildModelConverterFor( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
+
+		let modelElement = new ModelElement( 'div', null, new ModelElement( 'paragraph', null, 'foobar' ) );
+		modelRoot.appendChildren( modelElement );
+
+		dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+
+		expect( viewToString( viewRoot ) ).to.equal( '<div><div><p>foobar</p></div></div>' );
+	} );
+} );

+ 379 - 0
packages/ckeditor5-engine/tests/treecontroller/view-converter-builder.js

@@ -0,0 +1,379 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treecontroller */
+
+'use strict';
+
+import BuildViewConverterFor from '/ckeditor5/engine/treecontroller/view-converter-builder.js';
+
+import ModelSchema from '/ckeditor5/engine/treemodel/schema.js';
+import ModelDocument from '/ckeditor5/engine/treemodel/document.js';
+import ModelElement from '/ckeditor5/engine/treemodel/element.js';
+import ModelTextProxy from '/ckeditor5/engine/treemodel/textproxy.js';
+import ModelRange from '/ckeditor5/engine/treemodel/range.js';
+import ModelWalker from '/ckeditor5/engine/treemodel/treewalker.js';
+
+import ViewDocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
+import ViewContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
+import ViewAttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
+import ViewText from '/ckeditor5/engine/treeview/text.js';
+import ViewMatcher from '/ckeditor5/engine/treeview/matcher.js';
+
+import ViewConversionDispatcher from '/ckeditor5/engine/treecontroller/viewconversiondispatcher.js';
+
+import { convertToModelFragment, convertText } from '/ckeditor5/engine/treecontroller/view-to-model-converters.js';
+
+function modelAttributesToString( item ) {
+	let result = '';
+
+	for ( let attr of item.getAttributes() ) {
+		result += ' ' + attr[ 0 ] + '="' + attr[ 1 ] + '"';
+	}
+
+	return result;
+}
+
+function modelToString( item ) {
+	let result = '';
+
+	if ( item instanceof ModelTextProxy ) {
+		let attributes = modelAttributesToString( item );
+
+		result = attributes ? '<$text' + attributes + '>' + item.text + '</$text>' : item.text;
+	} else {
+		let walker = new ModelWalker( { boundaries: ModelRange.createFromElement( item ), shallow: true } );
+
+		for ( let value of walker ) {
+			result += modelToString( value.item );
+		}
+
+		if ( item instanceof ModelElement ) {
+			let attributes = modelAttributesToString( item );
+
+			result = '<' + item.name + attributes + '>' + result + '</' + item.name + '>';
+		}
+	}
+
+	return result;
+}
+
+const textAttributes = [ undefined, 'linkHref', 'linkTitle', 'bold', 'italic', 'style' ];
+const pAttributes = [ undefined, 'class', 'important', 'theme', 'decorated', 'size' ];
+
+describe( 'View converter builder', () => {
+	let dispatcher, modelDoc, modelRoot, schema, objWithContext;
+
+	beforeEach( () => {
+		// `additionalData` parameter for `.convert` calls.
+		objWithContext = { context: [ '$root' ] };
+
+		schema = new ModelSchema();
+
+		schema.registerItem( 'paragraph', '$block' );
+		schema.registerItem( 'div', '$block' );
+		schema.registerItem( 'customP', 'paragraph' );
+		schema.registerItem( 'image', '$inline' );
+		schema.registerItem( 'span', '$inline' );
+		schema.registerItem( 'MEGATRON', '$inline' ); // Yes, folks, we are building MEGATRON.
+		schema.registerItem( 'abcd', '$inline' );
+		schema.allow( { name: '$inline', attributes: textAttributes, inside: '$root' } );
+		schema.allow( { name: 'image', attributes: [ 'src' ], inside: '$root' } );
+		schema.allow( { name: 'image', attributes: [ 'src' ], inside: '$block' } );
+		schema.allow( { name: '$text', attributes: textAttributes, inside: '$block' } );
+		schema.allow( { name: '$text', attributes: textAttributes, inside: '$root' } );
+		schema.allow( { name: 'paragraph', attributes: pAttributes, inside: '$root' } );
+		schema.allow( { name: 'span', attributes: [ 'transformer' ], inside: '$root' } );
+		schema.allow( { name: 'div', attributes: [ 'class' ], inside: '$root' } );
+
+		dispatcher = new ViewConversionDispatcher( { schema } );
+		dispatcher.on( 'text', convertText() );
+
+		modelDoc = new ModelDocument();
+		modelRoot = modelDoc.createRoot( 'root', '$root' );
+	} );
+
+	it( 'should convert from view element to model element', () => {
+		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+
+		const result = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), objWithContext );
+		modelRoot.appendChildren( result );
+
+		expect( modelToString( result ) ).to.equal( '<paragraph>foo</paragraph>' );
+	} );
+
+	it( 'should convert from view element to model element using creator function', () => {
+		BuildViewConverterFor( dispatcher )
+			.fromElement( 'img' )
+			.toElement( ( viewElement ) => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) );
+
+		const result = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), objWithContext );
+		modelRoot.appendChildren( result );
+
+		expect( modelToString( result ) ).to.equal( '<image src="foo.jpg"></image>' );
+	} );
+
+	it( 'should convert from view element to model attribute', () => {
+		BuildViewConverterFor( dispatcher ).fromElement( 'strong' ).toAttributes( { bold: true } );
+
+		const result = dispatcher.convert( new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), objWithContext );
+		modelRoot.appendChildren( result );
+
+		// Have to check root because result is a ModelText.
+		expect( modelToString( modelRoot ) ).to.equal( '<$root><$text bold="true">foo</$text></$root>' );
+	} );
+
+	it( 'should convert from view element to multiple model attributes', () => {
+		// Very theoretical example...
+		BuildViewConverterFor( dispatcher ).fromElement( 'i' ).toAttributes( { italic: true, style: 'italic' } );
+
+		const result = dispatcher.convert( new ViewAttributeElement( 'i', null, new ViewText( 'foo' ) ), objWithContext );
+		modelRoot.appendChildren( result );
+
+		// Have to check root because result is a ModelText.
+		expect( modelToString( modelRoot ) ).to.equal( '<$root><$text italic="true" style="italic">foo</$text></$root>' );
+	} );
+
+	it( 'should convert from view element to model attributes using creator function', () => {
+		BuildViewConverterFor( dispatcher )
+			.fromElement( 'a' )
+			.toAttributes( ( viewElement ) => ( { linkHref: viewElement.getAttribute( 'href' ) } ) );
+
+		const result = dispatcher.convert( new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), objWithContext );
+		modelRoot.appendChildren( result );
+
+		// Have to check root because result is a ModelText.
+		expect( modelToString( modelRoot ) ).to.equal( '<$root><$text linkHref="foo.html">foo</$text></$root>' );
+	} );
+
+	it( 'should convert from view attribute to model attribute', () => {
+		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+
+		BuildViewConverterFor( dispatcher )
+			.fromAttribute( 'class' )
+			.toAttributes( ( viewElement ) => ( { class: viewElement.getAttribute( 'class' ) } ) );
+
+		const result = dispatcher.convert( new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext );
+		modelRoot.appendChildren( result );
+
+		expect( modelToString( result ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
+	} );
+
+	it( 'should convert from view attribute and key to model attribute', () => {
+		dispatcher.on( 'documentFragment', convertToModelFragment() );
+
+		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+		BuildViewConverterFor( dispatcher ).fromAttribute( 'class', 'important' ).toAttributes( { important: true } );
+		BuildViewConverterFor( dispatcher ).fromAttribute( 'class', 'theme-nice' ).toAttributes( { theme: 'nice' } );
+
+		const viewStructure = new ViewDocumentFragment( [
+			new ViewContainerElement( 'p', { class: 'important' }, new ViewText( 'foo' ) ),
+			new ViewContainerElement( 'p', { class: 'important theme-nice' }, new ViewText( 'bar' ) )
+		] );
+
+		const result = dispatcher.convert( viewStructure, objWithContext );
+
+		expect( modelToString( result ) )
+			.to.equal( '<paragraph important="true">foo</paragraph><paragraph important="true" theme="nice">bar</paragraph>' );
+	} );
+
+	it( 'should convert from multiple view entities to model attribute', () => {
+		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+
+		BuildViewConverterFor( dispatcher )
+			.fromElement( 'strong' )
+			.fromElement( 'b' )
+			.fromAttribute( 'class', 'bold' )
+			.fromAttribute( 'style', { 'font-weight': 'bold' } )
+			.toAttributes( { bold: true } );
+
+		const viewElement = new ViewContainerElement( 'p', null, [
+			new ViewAttributeElement( 'strong', null, new ViewText( 'aaa' ) ),
+			new ViewAttributeElement( 'b', null, new ViewText( 'bbb' ) ),
+			new ViewContainerElement( 'span', { class: 'bold' }, new ViewText( 'ccc' ) ),
+			new ViewContainerElement( 'span', { style: 'font-weight:bold; font-size:20px' }, new ViewText( 'ddd' ) )
+		] );
+
+		const result = dispatcher.convert( viewElement, objWithContext );
+		modelRoot.appendChildren( result );
+
+		expect( modelToString( result ) ).to.equal( '<paragraph><$text bold="true">aaabbbcccddd</$text></paragraph>' );
+	} );
+
+	it( 'should convert from pattern to model element', () => {
+		BuildViewConverterFor( dispatcher ).from(
+			{ name: 'span', class: 'megatron', attribute: { head: 'megatron', body: 'megatron', legs: 'megatron' } }
+		).toElement( 'MEGATRON' );
+
+		// Adding callbacks later so they are called later. MEGATRON callback is more important.
+		BuildViewConverterFor( dispatcher ).fromElement( 'span' ).toElement( 'span' );
+		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+
+		let result;
+
+		// Not quite megatron.
+		result = dispatcher.convert( new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), objWithContext );
+		modelRoot.appendChildren( result );
+		expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
+
+		// Almost a megatron. Missing a head.
+		result = dispatcher.convert(
+			new ViewContainerElement( 'span', { class: 'megatron', body: 'megatron', legs: 'megatron' }, new ViewText( 'foo' ) ),
+			objWithContext
+		);
+
+		modelRoot.appendChildren( result );
+		expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
+
+		// This would be a megatron but is a paragraph.
+		result = dispatcher.convert(
+			new ViewContainerElement(
+				'p',
+				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
+				new ViewText( 'foo' )
+			),
+			objWithContext
+		);
+
+		modelRoot.appendChildren( result );
+		expect( modelToString( result ) ).to.equal( '<paragraph>foo</paragraph>' );
+
+		// At last we have a megatron!
+		result = dispatcher.convert(
+			new ViewContainerElement(
+				'span',
+				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
+				new ViewText( 'foo' )
+			),
+			objWithContext
+		);
+
+		modelRoot.appendChildren( result );
+		expect( modelToString( result ) ).to.equal( '<MEGATRON>foo</MEGATRON>' );
+	} );
+
+	it( 'should convert from pattern to model attribute', () => {
+		BuildViewConverterFor( dispatcher ).fromElement( 'span' ).toElement( 'span' );
+
+		// This time without name so default span converter will convert children.
+		BuildViewConverterFor( dispatcher )
+			.from( { class: 'megatron', attribute: { head: 'megatron', body: 'megatron', legs: 'megatron' } } )
+			.toAttributes( { transformer: 'megatron' } );
+
+		let viewElement = new ViewContainerElement(
+			'span',
+			{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
+			new ViewText( 'foo' )
+		);
+
+		let result = dispatcher.convert( viewElement, objWithContext );
+
+		modelRoot.appendChildren( result );
+		expect( modelToString( result ) ).to.equal( '<span transformer="megatron">foo</span>' );
+	} );
+
+	it( 'should set different priorities for `toElement` and `toAttributes` conversion', () => {
+		BuildViewConverterFor( dispatcher )
+			.fromAttribute( 'class' )
+			.toAttributes( ( viewElement ) => ( { class: viewElement.getAttribute( 'class' ) } ) );
+		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+
+		let result = dispatcher.convert( new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext );
+		modelRoot.appendChildren( result );
+
+		// Element converter was fired first even though attribute converter was added first.
+		expect( modelToString( result ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
+	} );
+
+	it( 'should overwrite default priorities for converters', () => {
+		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+		BuildViewConverterFor( dispatcher )
+			.fromAttribute( 'class' )
+			.toAttributes( ( viewElement ) => ( { class: viewElement.getAttribute( 'class' ) } ) );
+
+		let result;
+
+		result = dispatcher.convert( new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext );
+		modelRoot.appendChildren( result );
+		expect( modelToString( result ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
+
+		BuildViewConverterFor( dispatcher )
+			.from( { name: 'p', class: 'myClass' } ).withPriority( -1 ) // Default for `toElement` is 0.
+			.toElement( 'customP' );
+
+		result = dispatcher.convert( new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext );
+		modelRoot.appendChildren( result );
+		expect( modelToString( result ) ).to.equal( '<customP>foo</customP>' );
+	} );
+
+	it( 'should overwrite default consumed values', () => {
+		// Converter (1).
+		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+
+		// Converter (2).
+		BuildViewConverterFor( dispatcher )
+			.from( { name: 'p', class: 'decorated' } ).consuming( { class: 'decorated' } )
+			.toAttributes( { decorated: true } );
+
+		// Converter (3).
+		BuildViewConverterFor( dispatcher )
+			.fromAttribute( 'class', 'small' ).consuming( { class: 'small' } )
+			.toAttributes( { size: 'small' } );
+
+		const viewElement = new ViewContainerElement( 'p', { class: 'decorated small' }, new ViewText( 'foo' ) );
+
+		const result = dispatcher.convert( viewElement, objWithContext );
+		modelRoot.appendChildren( result );
+
+		// P element and it's children got converted by the converter (1) and the converter (1) got fired
+		// because P name was not consumed in converter (2). Converter (3) could consume class="small" because
+		// only class="decorated" was consumed in converter (2).
+		expect( modelToString( result ) ).to.equal( '<paragraph decorated="true" size="small">foo</paragraph>' );
+	} );
+
+	it( 'should convert from matcher instance to model', () => {
+		// Universal class converter, synonymous to .fromAttribute( 'class' ).
+		BuildViewConverterFor( dispatcher )
+			.from( new ViewMatcher( { class: /.*/ } ) )
+			.toAttributes( ( viewElement ) => ( { class: viewElement.getAttribute( 'class' ) } ) );
+
+		// Universal element converter.
+		BuildViewConverterFor( dispatcher )
+			.from( new ViewMatcher( { name: /.*/ } ) )
+			.toElement( ( viewElement ) => new ModelElement( viewElement.name ) );
+
+		let viewStructure = new ViewContainerElement( 'div', { class: 'myClass' }, [
+			new ViewContainerElement( 'abcd', null, new ViewText( 'foo' ) )
+		] );
+
+		let result = dispatcher.convert( viewStructure, objWithContext );
+		modelRoot.appendChildren( result );
+
+		expect( modelToString( result ) ).to.equal( '<div class="myClass"><abcd>foo</abcd></div>' );
+	} );
+
+	it( 'should filter out structure that is wrong with schema', () => {
+		BuildViewConverterFor( dispatcher ).fromElement( 'strong' ).toAttributes( { bold: true } );
+		BuildViewConverterFor( dispatcher ).fromElement( 'div' ).toElement( 'div' );
+		BuildViewConverterFor( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+
+		schema.disallow( { name: '$text', attributes: 'bold', inside: 'paragraph' } );
+		schema.disallow( { name: 'div', inside: '$root' } );
+
+		dispatcher.on( 'element', convertToModelFragment() );
+
+		let viewElement = new ViewContainerElement( 'div', null,
+			new ViewContainerElement( 'p', null,
+				new ViewAttributeElement( 'strong', null,
+					new ViewText( 'foo' )
+				)
+			)
+		);
+
+		let result = dispatcher.convert( viewElement, objWithContext );
+
+		expect( modelToString( result ) ).to.equal( '<paragraph>foo</paragraph>' );
+	} );
+} );