8
0
Просмотр исходного кода

Changed: Unified converters config.
Fixed: Classes and styles are properly handled in downcast converters.
Docs: Fixed converters docs.
Changed: Support for passing element name in `downcastAttributeToAttribute` converter.
Added: Introduced `conversion.ConverterDefinition` type.
Added: Added priority handling in `conversion.Conversion` converters.
Changed: Cleaned low-level converters after they can receive only element creator function as a parameter.

Szymon Cofalik 8 лет назад
Родитель
Сommit
25f30a5e65

+ 219 - 207
packages/ckeditor5-engine/src/conversion/conversion.js

@@ -115,37 +115,18 @@ export default class Conversion {
 	}
 
 	/**
-	 * Returns dispatchers registered under given group name.
-	 *
-	 * If given group name has not been registered,
-	 * {@link module:utils/ckeditorerror~CKEditorError conversion-for-unknown-group} error is thrown.
-	 *
-	 * @private
-	 * @param {String} groupName
-	 * @returns {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
-	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>}
-	 */
-	_getDispatchers( groupName ) {
-		const dispatchers = this._dispatchersGroups.get( groupName );
-
-		if ( !dispatchers ) {
-			/**
-			 * Trying to add a converter to an unknown dispatchers group.
-			 *
-			 * @error conversion-for-unknown-group
-			 */
-			throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' );
-		}
-
-		return dispatchers;
-	}
-
-	/**
-	 * Defines a conversion between the model and the view where a model element is represented as a view element (and vice versa).
+	 * Sets up converters between the model and the view which convert a model element to a view element (and vice versa).
 	 * For example, model `<paragraph>Foo</paragraph>` is `<p>Foo</p>` in the view.
 	 *
+	 * `definition.model` is a `String` with a model element name to converter from/to.
+	 *
+	 *		// Simple conversion from `paragraph` model element to `<p>` view element (and vice versa).
 	 *		conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 	 *
+	 *		// Override other converters by specifying converter definition with higher priority.
+	 *		conversion.elementToElement( { model: 'paragraph', view: 'div', priority: 'high' } );
+	 *
+	 *		// View specified as an object instead of a string.
 	 *		conversion.elementToElement( {
 	 *			model: 'fancyParagraph',
 	 *			view: {
@@ -154,13 +135,14 @@ export default class Conversion {
 	 *			}
 	 *		} );
 	 *
+	 *		// Use `upcastAlso` to define other view elements that should be also converted to `paragraph` element.
 	 *		conversion.elementToElement( {
 	 *			model: 'paragraph',
 	 *			view: 'p',
 	 *			upcastAlso: [
 	 *				'div',
 	 *				{
-	 *					// Match any name.
+	 *					// Any element with `display: block` style.
 	 *					name: /./,
 	 *					style: {
 	 *						display: 'block'
@@ -169,6 +151,7 @@ export default class Conversion {
 	 *			]
 	 *		} );
 	 *
+	 *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
 	 *		conversion.elementToElement( {
 	 *			model: 'heading',
 	 *			view: 'h2',
@@ -196,41 +179,49 @@ export default class Conversion {
 	 *			}
 	 *		} );
 	 *
-	 * @param {Object} definition Conversion definition.
-	 * @param {String} definition.model Name of the model element to convert.
-	 * @param {module:engine/view/elementdefinition~ElementDefinition} definition.view Definition of a view element to convert from/to.
-	 * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
-	 * Any view element matching `upcastAlso` will also be converted to the given model element.
+	 * @param {~ConverterDefinition} definition Converter definition.
 	 */
 	elementToElement( definition ) {
 		// Set up downcast converter.
 		this.for( 'downcast' ).add( downcastElementToElement( definition ) );
 
 		// Set up upcast converter.
-		for ( const view of _getAllViews( definition ) ) {
-			const priority = view == definition.view ? 'normal' : 'high';
-
-			this.for( 'upcast' ).add( upcastElementToElement( {
-				model: definition.model,
-				view
-			}, priority ) );
+		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
+			this.for( 'upcast' ).add(
+				upcastElementToElement( {
+					model,
+					view,
+					priority: definition.priority
+				} )
+			);
 		}
 	}
 
 	/**
-	 * Defines a conversion between the model and the view where a model attribute is represented as a view element (and vice versa).
+	 * Sets up converters between the model and the view which convert a model attribute to a view element (and vice versa).
 	 * For example, model text node with data `"Foo"` and `bold` attribute is `<strong>Foo</strong>` in the view.
 	 *
-	 *		conversion.attributeToElement( 'bold', { view: 'strong' } );
+	 * `definition.model` parameter specifies what model attribute should be converted from/to. It can be a `{ key, value }` object
+	 * describing attribute key and value to convert or a `String` specifying just attribute key (then `value` is set to `true`).
+	 *
+	 *		// Simple conversion from `bold=true` attribute to `<strong>` view element (and vice versa).
+	 *		conversion.attributeToElement( { model: 'bold', view: 'strong' } );
 	 *
-	 *		conversion.attributeToElement( 'bold', {
+	 *		// Override other converters by specifying converter definition with higher priority.
+	 *		conversion.attributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
+	 *
+	 *		// View specified as an object instead of a string.
+	 *		conversion.attributeToElement( {
+	 *			model: 'bold',
 	 *			view: {
 	 *				name: 'span',
 	 *				class: 'bold'
 	 *			}
 	 *		} );
 	 *
-	 *		conversion.attributeToElement( 'bold', {
+	 *		// Use `upcastAlso` to define other view elements that should be also converted to `bold` attribute.
+	 *		conversion.attributeToElement( {
+	 *			model: 'bold',
 	 *			view: 'strong',
 	 *			upcastAlso: [
 	 *				'b',
@@ -257,45 +248,29 @@ export default class Conversion {
 	 *			]
 	 *		} );
 	 *
-	 *		conversion.attributeToElement( 'styled', {
-	 *			model: 'dark',
+	 *		// Conversion from/to a model attribute key which value is an enum (`fontSize=big|small`).
+	 *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
+	 *		conversion.attributeToElement( {
+	 *			model: {
+	 *				key: 'fontSize',
+	 *				values: [ 'big', 'small' ]
+	 *			},
 	 *			view: {
-	 *				name: 'span',
-	 *				class: [ 'styled', 'styled-dark' ]
-	 *			}
-	 *		} );
-	 *
-	 *		conversion.attributeToElement( 'fontSize', [
-	 *			{
-	 *				model: 'big',
-	 *				view: {
+	 *				big: {
 	 *					name: 'span',
 	 *					style: {
 	 *						'font-size': '1.2em'
 	 *					}
-	 *				}
-	 *			},
-	 *			{
-	 *				model: 'small',
-	 *				view: {
+	 *				},
+	 *				small: {
 	 *					name: 'span',
 	 *					style: {
 	 *						'font-size': '0.8em'
 	 *					}
 	 *				}
-	 *			}
-	 *		] );
-	 *
-	 *		conversion.attributeToElement( 'fontSize', [
-	 *			{
-	 *				model: 'big',
-	 *				view: {
-	 *					name: 'span',
-	 *					style: {
-	 *						'font-size': '1.2em'
-	 *					}
-	 *				},
-	 *				upcastAlso: viewElement => {
+	 *			},
+	 *			upcastAlso: {
+	 *				big: viewElement => {
 	 *					const fontSize = viewElement.getStyle( 'font-size' );
 	 *
 	 *					if ( !fontSize ) {
@@ -315,17 +290,8 @@ export default class Conversion {
 	 *					}
 	 *
 	 *					return null;
-	 *				}
-	 *			},
-	 *			{
-	 *				model: 'small',
-	 *				view: {
-	 *					name: 'span',
-	 *					style: {
-	 *						'font-size': '0.8em'
-	 *					}
 	 *				},
-	 *				upcastAlso: viewElement => {
+	 *				small: viewElement => {
 	 *					const fontSize = viewElement.getStyle( 'font-size' );
 	 *
 	 *					if ( !fontSize ) {
@@ -347,146 +313,192 @@ export default class Conversion {
 	 *					return null;
 	 *				}
 	 *			}
-	 *		] );
+	 *		} );
 	 *
-	 * @param {String} modelAttributeKey The key of the model attribute to convert.
-	 * @param {Object|Array.<Object>} definition Conversion definition. It is possible to provide multiple definitions in an array.
-	 * @param {*} [definition.model] The value of the converted model attribute. If omitted, when downcasted, the item will be treated
-	 * as a default item, that will be used when no other item matches. When upcasted, the model attribute value will be set to `true`.
-	 * @param {module:engine/view/elementdefinition~ElementDefinition} definition.view Definition of a view element to convert from/to.
-	 * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
-	 * Any view element matching `upcastAlso` will also be converted to the given model element.
+	 * @param {~ConverterDefinition} definition Converter definition.
 	 */
-	attributeToElement( modelAttributeKey, definition ) {
-		// Set downcast (model to view conversion).
-		this.for( 'downcast' ).add( downcastAttributeToElement( modelAttributeKey, definition ) );
-
-		// Set upcast (view to model conversion). In this case, we need to re-organise the definition config.
-		if ( !Array.isArray( definition ) ) {
-			definition = [ definition ];
-		}
-
-		for ( const item of definition ) {
-			const model = _getModelAttributeDefinition( modelAttributeKey, item.model );
-
-			for ( const view of _getAllViews( item ) ) {
-				const priority = view == item.view ? 'normal' : 'high';
+	attributeToElement( definition ) {
+		// Set up downcast converter.
+		this.for( 'downcast' ).add( downcastAttributeToElement( definition ) );
 
-				this.for( 'upcast' ).add( upcastElementToAttribute( {
+		// Set up upcast converter.
+		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
+			this.for( 'upcast' ).add(
+				upcastElementToAttribute( {
 					view,
-					model
-				}, priority ) );
-			}
+					model,
+					priority: definition.priority
+				} )
+			);
 		}
 	}
 
 	/**
-	 * Defines a conversion between the model and the view where a model attribute is represented as a view attribute (and vice versa).
-	 * For example, `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>` (same attribute name and value).
-	 *
-	 *		conversion.attributeToAttribute( 'src' );
-	 *
-	 *		conversion.attributeToAttribute( 'source', { view: 'src' } );
-	 *
-	 *		conversion.attributeToAttribute( 'aside', {
-	 *			model: true,
+	 * Sets up converters between the model and the view which convert a model attribute to a view attribute (and vice versa).
+	 * For example, `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>` (same attribute key and value).
+	 *
+	 * `definition.model` parameter specifies what model attribute should be converted from/to.
+	 * It can be a `{ key, values, [ name ] }` object or a `String`, which will be treated like `{ key: definition.model }`.
+	 * `key` property is the model attribute key to convert from/to.
+	 * `values` are the possible model attribute values. If `values` is not set, model attribute value will be the same as the
+	 * view attribute value.
+	 * If `name` is set, conversion will be set up only for model elements with the given name.
+	 *
+	 * `definition.view` parameter specifies what view attribute should be converted from/to.
+	 * It can be a `{ key, value, [ name ] }` object or a `String`, which will be treated like `{ key: definition.view }`.
+	 * `key` property is the view attribute key to convert from/to.
+	 * `value` is the view attribute value to convert from/to. If `definition.value` is not set, view attribute value will be
+	 * the same as the model attribute value.
+	 * If `key` is `'class'`, `value` can be a `String` or an array of `String`s.
+	 * If `key` is `'style'`, `value` is an object with key-value pairs.
+	 * In other cases, `value` is a `String`.
+	 * If `name` is set, conversion will be set up only for model elements with the given name.
+	 * If `definition.model.values` is set, `definition.view` is an object which assigns values from `definition.model.values`
+	 * to `{ key, value, [ name ] }` objects.
+	 *
+	 * `definition.upcastAlso` specifies which other matching view elements should be also upcast to given model configuration.
+	 * If `definition.model.values` is set, `definition.upcastAlso` should be an object assigning values from `definition.model.values`
+	 * to {@link module:engine/view/matcher~MatcherPattern}s or arrays of {@link module:engine/view/matcher~MatcherPattern}s.
+	 *
+	 * **Note:** `definition.model` and `definition.view` form should be mirrored, that is the same type of parameters should
+	 * be given in both parameters.
+	 *
+	 *		// Simple conversion from `source` model attribute to `src` view attribute (and vice versa).
+	 *		conversion.attributeToAttribute( { model: 'source', view: 'src' } );
+	 *
+	 *		// Attributes values are strictly specified.
+	 *		conversion.attributeToAttribute( {
+	 *			model: {
+	 *				name: 'image',
+	 *				key: 'aside',
+	 *				values: [ 'aside' ]
+	 *			},
 	 *			view: {
-	 *				name: 'img',
-	 *				key: 'class',
-	 *				value: 'aside half-size'
+	 *				aside: {
+	 *					name: 'img',
+	 *					key: 'class',
+	 *					value: [ 'aside', 'half-size' ]
+	 *				}
 	 *			}
 	 *		} );
 	 *
-	 *		conversion.attributeToAttribute( 'styled', [
-	 *			{
-	 *				model: 'dark',
-	 *				view: {
-	 *					key: 'class',
-	 *					value: 'styled styled-dark'
-	 *				}
+	 *		// Set style attribute.
+	 *		conversion.attributeToAttribute( {
+	 *			model: {
+	 *				name: 'image',
+	 *				key: 'aside',
+	 *				values: [ 'aside' ]
 	 *			},
-	 *			{
-	 *				model: 'light',
-	 *				view: {
-	 *					key: 'class',
-	 *					value: 'styled styled-light'
+	 *			view: {
+	 *				aside: {
+	 *					name: 'img',
+	 *					key: 'style',
+	 *					value: {
+	 *						float: 'right',
+	 *						width: '50%',
+	 *						margin: '5px'
+	 *					}
 	 *				}
 	 *			}
-	 *		] );
+	 *		} );
 	 *
-	 *		conversion.attributeToAttribute( 'align', [
-	 *			{
-	 *				model: 'right',
-	 *				view: {
+	 *		// Conversion from/to a model attribute key which value is an enum (`align=right|center`).
+	 *		// Use `upcastAlso` to define other view elements that should be also converted to `align=right` attribute.
+	 *		conversion.attributeToAttribute( {
+	 *			model: {
+	 *				key: 'align',
+	 *				values: [ 'right', 'center' ]
+	 *			},
+	 *			view: {
+	 *				right: {
 	 *					key: 'class',
 	 *					value: 'align-right'
 	 *				},
-	 *				upcastAlso: viewElement => {
-	 *					if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
-	 *						return {
-	 *							style: [ 'text-align' ]
-	 *						};
-	 *					}
-	 *
-	 *					return null;
-	 *				}
-	 *			},
-	 *			{
-	 *				model: 'center',
-	 *				view: {
+	 *				center: {
 	 *					key: 'class',
 	 *					value: 'align-center'
+	 *				}
+	 *			},
+	 *			upcastAlso: {
+	 *				right: {
+	 *					style: {
+	 *						'text-align': 'right'
+	 *					}
 	 *				},
-	 *				upcastAlso: {
+	 *				center: {
 	 *					style: {
 	 *						'text-align': 'center'
 	 *					}
 	 *				}
 	 *			}
-	 *		] );
-	 *
-	 * @param {String} modelAttributeKey The key of the model attribute to convert.
-	 * @param {Object|Array.<Object>} [definition] Conversion definition. It is possible to provide multiple definitions in an array.
-	 * If not set, the conversion helper will assume 1-to-1 conversion, that is the model attribute key and value will be same
-	 * as the view attribute key and value.
-	 * @param {*} [definition.model] The value of the converted model attribute. If omitted, when downcasting,
-	 * the item will be treated as a default item, that will be used when no other item matches. When upcasting conversion,
-	 * the model attribute value will be set to the same value as in the view.
-	 * @param {Object} definition.view View attribute conversion details. Given object has required `key` property,
-	 * specifying view attribute key, optional `value` property, specifying view attribute value and optional `name`
-	 * property specifying a view element name from/on which the attribute should be converted. If `value` is not given,
-	 * the view attribute value will be equal to model attribute value.
+	 *		} );
+	 *
+	 * @param {Object} [definition] Converter definition.
+	 * @param {String|Object} definition.model Model attribute to convert from/to.
+	 * @param {String|Object} definition.view View attribute to convert from/to.
 	 * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
-	 * Any view element matching `upcastAlso` will also be converted to the given model element.
+	 * Any view element matching `definition.upcastAlso` will also be converted to the given model attribute. `definition.upcastAlso`
+	 * is used only if `config.model.values` is specified.
 	 */
-	attributeToAttribute( modelAttributeKey, definition ) {
+	attributeToAttribute( definition ) {
 		// Set up downcast converter.
-		this.for( 'downcast' ).add( downcastAttributeToAttribute( modelAttributeKey, definition ) );
-
-		// Set up upcast converter. In this case, we need to re-organise the definition config.
-		if ( !definition ) {
-			definition = { view: modelAttributeKey };
-		}
+		this.for( 'downcast' ).add( downcastAttributeToAttribute( definition ) );
 
-		if ( !Array.isArray( definition ) ) {
-			definition = [ definition ];
+		// Set up upcast converter.
+		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
+			this.for( 'upcast' ).add(
+				upcastAttributeToAttribute( {
+					view,
+					model
+				} )
+			);
 		}
+	}
 
-		for ( const item of definition ) {
-			const model = _getModelAttributeDefinition( modelAttributeKey, item.model );
-
-			for ( const view of _getAllViews( item ) ) {
-				const priority = view == item.view ? 'low' : 'normal';
+	/**
+	 * Returns dispatchers registered under given group name.
+	 *
+	 * If given group name has not been registered,
+	 * {@link module:utils/ckeditorerror~CKEditorError conversion-for-unknown-group} error is thrown.
+	 *
+	 * @private
+	 * @param {String} groupName
+	 * @returns {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>}
+	 */
+	_getDispatchers( groupName ) {
+		const dispatchers = this._dispatchersGroups.get( groupName );
 
-				this.for( 'upcast' ).add( upcastAttributeToAttribute( {
-					view,
-					model
-				}, priority ) );
-			}
+		if ( !dispatchers ) {
+			/**
+			 * Trying to add a converter to an unknown dispatchers group.
+			 *
+			 * @error conversion-for-unknown-group
+			 */
+			throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' );
 		}
+
+		return dispatchers;
 	}
 }
 
+/**
+ * Defines how the model should be converted from/to the view.
+ *
+ * @typedef {Object} module:engine/conversion/conversion~ConverterDefinition
+ *
+ * @property {*} [model] Model conversion definition. Describes model element or model attribute to convert. This parameter differs
+ * for different functions that accepts `ConverterDefinition`. See the description of a function to learn how to set it.
+ * @property {module:engine/view/elementdefinition~ElementDefinition|Object} view Definition of a view element to convert from/to.
+ * If `model` describes multiple values, `view` is an object that assigns those values (`view` object keys) to view element definitions
+ * (`view` object values).
+ * @property {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [upcastAlso]
+ * Any view element matching `upcastAlso` will also be converted to model. If `model` describes multiple values, `upcastAlso`
+ * is an object that assigns those values (`upcastAlso` object keys) to {@link module:engine/view/matcher~MatcherPattern}s
+ * (`upcastAlso` object values).
+ * @property {module:utils/priorities~PriorityString} [priority] Conversion priority.
+ */
+
 // Helper function for `Conversion` `.add()` method.
 //
 // Calls `conversionHelper` on each dispatcher from the group specified earlier in `.for()` call, effectively
@@ -502,33 +514,33 @@ function _addToDispatchers( dispatchers, conversionHelper ) {
 	}
 }
 
-// Helper function, normalizes input data into a correct config form that can be accepted by conversion helpers. The
-// correct form is either `String` or an object with `key` and `value` properties.
-//
-// @param {String} key Model attribute key.
-// @param {*} [model] Model attribute value.
-// @returns {Object} Normalized model attribute definition.
-function _getModelAttributeDefinition( key, model ) {
-	if ( model === undefined ) {
-		return key;
-	} else {
-		return {
-			key, value: model
-		};
-	}
-}
-
 // Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
 // `definition.upcastAlso`.
 //
-// @param {Object} definition Conversion definition.
+// @param {~ConverterDefinition} definition
 // @returns {Array} Array containing view definitions.
-function _getAllViews( definition ) {
-	if ( !definition.upcastAlso ) {
-		return [ definition.view ];
+function* _getAllUpcastDefinitions( definition ) {
+	if ( definition.model.values ) {
+		for ( const value of definition.model.values ) {
+			const model = { key: definition.model.key, value };
+			const view = definition.view[ value ];
+			const upcastAlso = definition.upcastAlso ? definition.upcastAlso[ value ] : undefined;
+
+			yield* _getUpcastDefinition( model, view, upcastAlso );
+		}
 	} else {
-		const upcastAlso = Array.isArray( definition.upcastAlso ) ? definition.upcastAlso : [ definition.upcastAlso ];
+		yield* _getUpcastDefinition( definition.model, definition.view, definition.upcastAlso );
+	}
+}
+
+function* _getUpcastDefinition( model, view, upcastAlso ) {
+	yield { model, view };
 
-		return [ definition.view ].concat( upcastAlso );
+	if ( upcastAlso ) {
+		upcastAlso = Array.isArray( upcastAlso ) ? upcastAlso : [ upcastAlso ];
+
+		for ( const upcastAlsoItem of upcastAlso ) {
+			yield { model, view: upcastAlsoItem };
+		}
 	}
 }

+ 220 - 256
packages/ckeditor5-engine/src/conversion/downcast-converters.js

@@ -7,9 +7,6 @@ import ModelRange from '../model/range';
 import ModelSelection from '../model/selection';
 import ModelElement from '../model/element';
 
-import ViewContainerElement from '../view/containerelement';
-import ViewUIElement from '../view/uielement';
-import ViewElement from '../view/element';
 import ViewAttributeElement from '../view/attributeelement';
 import ViewRange from '../view/range';
 import DocumentSelection from '../model/documentselection';
@@ -29,7 +26,7 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  *
  *		downcastElementToElement( { model: 'paragraph', view: 'p' } );
  *
- *		downcastElementToElement( { model: 'paragraph', view: 'p' }, 'high' );
+ *		downcastElementToElement( { model: 'paragraph', view: 'div', priority: 'high' } );
  *
  *		downcastElementToElement( {
  *			model: 'fancyParagraph',
@@ -41,33 +38,24 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  *
  * 		downcastElementToElement( {
  * 			model: 'heading',
- * 			view: ( modelItem, consumable, conversionApi ) => {
- * 				const viewWriter = conversionApi.writer;
- *
- * 				return viewWriter.createContainerElement( 'h' + modelItem.getAttribute( 'level' ) );
- * 			}
+ * 			view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
  * 		} );
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
  * @param {Object} config Conversion configuration.
  * @param {String} config.model Name of the model element to convert.
- * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element name, or a
- * view element definition, or a function that  will be provided with all the parameters of the dispatcher's
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}.
- * It's expected that the function returns a {@link module:engine/view/containerelement~ContainerElement}.
- * The view element will be used then in conversion.
- *
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element definition or a function
+ * that takes model element and view writer as a parameters and returns a view container element.
  * @returns {Function} Conversion helper.
  */
-export function downcastElementToElement( config, priority = 'normal' ) {
+export function downcastElementToElement( config ) {
 	config = cloneDeep( config );
 
-	_normalizeToElementConfig( config, ViewContainerElement );
+	config.view = _normalizeToElementConfig( config.view, 'container' );
 
 	return dispatcher => {
-		dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority } );
+		dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.priority || 'normal' } );
 	};
 }
 
@@ -77,75 +65,74 @@ export function downcastElementToElement( config, priority = 'normal' ) {
  * This conversion results in wrapping view nodes in a view attribute element. For example, model text node with data
  * `"Foo"` and `bold` attribute becomes `<strong>Foo</strong>` in the view.
  *
- *		downcastAttributeToElement( 'bold', { view: 'strong' } );
+ *		downcastAttributeToElement( { model: 'bold', view: 'strong' } );
  *
- *		downcastAttributeToElement( 'bold', { view: 'strong' }, 'high' );
+ *		downcastAttributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
  *
- *		downcastAttributeToElement( 'bold', {
+ *		downcastAttributeToElement( {
+ *			model: 'invert',
  *			view: {
  *				name: 'span',
- *				class: 'bold'
+ *				class: [ 'font-light', 'bg-dark' ]
  *			}
  *		} );
  *
- *		downcastAttributeToElement( 'styled', {
- *			model: 'dark',
+ *		downcastAttributeToElement( {
+ *			model: {
+ *				key: 'fontSize',
+ *				values: [ 'big', 'small' ]
+ *			},
  *			view: {
- *				name: 'span',
- *				class: [ 'styled', 'styled-dark' ]
- *			}
- *		} );
- *
- *		downcastAttributeToElement( 'fontSize', [
- *			{
- *				model: 'big',
- *				view: {
+ *				big: {
  *					name: 'span',
  *					style: {
  *						'font-size': '1.2em'
  *					}
- *				}
- *			},
- *			{
- *				model: 'small',
- *				view: {
+ *				},
+ *				small: {
  *					name: 'span',
  *					style: {
  *						'font-size': '0.8em'
  *					}
  *				}
  *			}
- *		] );
- *
- * 		downcastAttributeToElement( 'bold', {
- * 			view: ( modelAttributeValue, data, consumable, conversionApi ) => {
- * 				const viewWriter = conversionApi.writer;
+ *		} );
  *
- * 				return viewWriter( 'span', { style: 'font-weight:' + modelAttributeValue } );
+ * 		downcastAttributeToElement( {
+ * 			model: 'bold',
+ * 			view: ( modelAttributeValue, viewWriter ) => {
+ * 				return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
  * 			}
  * 		} );
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
- * @param {String} modelAttributeKey The key of the attribute to convert.
- * @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
- * @param {*} [config.model] The value of the converted model attribute for which the `view` property is defined.
- * If omitted, the configuration item will be used as a "default" configuration when no other item matches the attribute value.
- * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element name,
- * or a view element definition, or a function that takes model element as a parameter and returns a view attribute element.
- * The view element will be used then in conversion.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {Object} config Conversion configuration.
+ * @param {String|Object} config.model Key of the attribute to convert from or a `{ key, values }` object. `values` is an array
+ * of `String`s with possible values if the model attribute is enumerable.
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view View element definition or a function
+ * that takes model attribute value and view writer as parameters and returns a view attribute element. If `config.model.values` is
+ * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  */
-export function downcastAttributeToElement( modelAttributeKey, config, priority = 'normal' ) {
+export function downcastAttributeToElement( config ) {
 	config = cloneDeep( config );
 
-	_normalizeToElementConfig( config, ViewAttributeElement );
+	const modelKey = config.model.key ? config.model.key : config.model;
+
+	if ( config.model.values ) {
+		for ( const modelValue of config.model.values ) {
+			config.view[ modelValue ] = _normalizeToElementConfig( config.view[ modelValue ], 'attribute' );
+		}
+	} else {
+		config.view = _normalizeToElementConfig( config.view, 'attribute' );
+	}
 
-	const elementCreator = _getCreatorForArrayConfig( config );
+	const elementCreator = _getFromAttributeCreator( config );
 
 	return dispatcher => {
-		dispatcher.on( 'attribute:' + modelAttributeKey, wrap( elementCreator ), { priority } );
+		dispatcher.on( 'attribute:' + modelKey, wrap( elementCreator ), { priority: config.priority || 'normal' } );
 	};
 }
 
@@ -155,74 +142,75 @@ export function downcastAttributeToElement( modelAttributeKey, config, priority
  * This conversion results in adding an attribute on a view node, basing on an attribute from a model node. For example,
  * `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>`.
  *
- *		downcastAttributeToAttribute( 'src' );
- *
- *		downcastAttributeToAttribute( 'source', { view: 'src' } );
+ *		downcastAttributeToAttribute( { model: 'source', view: 'src' } );
  *
- *		downcastAttributeToAttribute( 'source', { view: 'src' }, 'high' );
+ *		downcastAttributeToAttribute( { model: 'source', view: 'href', priority: 'high' } );
  *
- *		downcastAttributeToAttribute( 'stylish', {
- *			view: {
- *				key: 'class',
- *				value: 'styled'
- *			}
+ *		downcastAttributeToAttribute( {
+ *			model: {
+ *				name: 'image',
+ *				key: 'source'
+ *			},
+ *			view: 'src'
  *		} );
  *
- *		downcastAttributeToAttribute( 'styled', {
- *			model: 'dark',
+ *		downcastAttributeToAttribute( {
+ *			model: {
+ *				name: 'styled',
+ *				values: [ 'dark', 'light' ]
+ *			},
  *			view: {
- *				key: 'class',
- *				value: 'styled styled-dark'
- *			}
- *		} );
- *
- *		downcastAttributeToAttribute( 'style', [
- *			{
- *				model: 'dark',
- *				view: {
+ *				dark: {
  *					key: 'class',
- *					value: 'styled-dark'
- *				}
- *			},
- *			{
- *				model: 'light',
- *				view: {
+ *					value: [ 'styled', 'styled-dark' ]
+ *				},
+ *				light: {
  *					key: 'class',
- *					value: 'styled-light'
+ *					value: [ 'styled', 'styled-light' ]
  *				}
  *			}
- *		] );
+ *		} );
  *
- *		downcastAttributeToAttribute( 'style', {
- *			view: attributeValue => ( { key: 'class', value: 'style-' + attributeValue } )
+ *		downcastAttributeToAttribute( {
+ *			model: 'styled',
+ *			view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } )
  *		} );
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
- * @param {String} modelAttributeKey The key of the attribute to convert.
- * @param {Object|Array.<Object>} [config] Conversion configuration. It is possible to provide multiple configurations in an array.
- * If not set, the conversion helper will assume 1-to-1 conversion, that is the view attribute key and view attribute value
- * will be same as model attribute key and model attribute value.
- * @param {*} [config.model] The value of the converted model attribute for which the `view` property is defined.
- * If `true` is provided, the configuration item will be used as a "default" configuration when no other item matches
- * the attribute value.
- * @param {String|Object|Function} [config.view] View attribute key, or an object with `key` and `value` properties (both `String`),
- * or a function that takes model attribute value and returns an object with `key` and `value` properties (both `String`).
- * If nothing is passed, the view attribute key and value will be equal to the model attribute key and value.
- * If a `String` is passed, it will be used as view attribute key and view attribute value will be equal to model attribute value.
- * If an object or a function returning an object is passed, its properties will be used to set view attribute key and value.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {Object} config Conversion configuration.
+ * @param {String|Object} config.model Key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
+ * the attribute key, possible values and, optionally, an element name to convert from.
+ * @param {String|Object|Function} config.view View attribute key, or a `{ key, value }` object or a function that takes
+ * model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an
+ * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`.
+ * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
+ * `{ key, value }` objects or a functions.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  */
-export function downcastAttributeToAttribute( modelAttributeKey, config = {}, priority = 'normal' ) {
+export function downcastAttributeToAttribute( config ) {
 	config = cloneDeep( config );
 
-	_normalizeToAttributeConfig( modelAttributeKey, config );
+	const modelKey = config.model.key ? config.model.key : config.model;
+	let eventName = 'attribute:' + modelKey;
 
-	const elementCreator = _getCreatorForArrayConfig( config );
+	if ( config.model.name ) {
+		eventName += ':' + config.model.name;
+	}
+
+	if ( config.model.values ) {
+		for ( const modelValue of config.model.values ) {
+			config.view[ modelValue ] = _normalizeToAttributeConfig( config.view[ modelValue ] );
+		}
+	} else {
+		config.view = _normalizeToAttributeConfig( config.view );
+	}
+
+	const elementCreator = _getFromAttributeCreator( config );
 
 	return dispatcher => {
-		dispatcher.on( 'attribute:' + modelAttributeKey, changeAttribute( elementCreator ), { priority } );
+		dispatcher.on( eventName, changeAttribute( elementCreator ), { priority: config.priority || 'normal' } );
 	};
 }
 
@@ -235,9 +223,7 @@ export function downcastAttributeToAttribute( modelAttributeKey, config = {}, pr
  *
  *		downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
  *
- *		downcastMarkerToElement( { model: 'search', view: 'marker-search' }, 'high' );
- *
- *		downcastMarkerToElement( { model: 'search', view: new ViewUIElement( 'span', { data-marker: 'search' } ) } );
+ *		downcastMarkerToElement( { model: 'search', view: 'search-result', priority: 'high' } );
  *
  *		downcastMarkerToElement( {
  *			model: 'search',
@@ -251,8 +237,8 @@ export function downcastAttributeToAttribute( modelAttributeKey, config = {}, pr
  *
  * 		downcastMarkerToElement( {
  * 			model: 'search',
- * 			view: ( data, conversionApi ) => {
- *	 			return conversionApi.writer.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
+ * 			view: ( markerData, viewWriter ) => {
+ *	 			return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': markerData.isOpening } );
  * 			}
  * 		} );
  *
@@ -269,20 +255,19 @@ export function downcastAttributeToAttribute( modelAttributeKey, config = {}, pr
  *
  * @param {Object} config Conversion configuration.
  * @param {String} config.model Name of the model marker (or model marker group) to convert.
- * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element definition
- * which will be used to build a view element for conversion or a function that takes model marker data as a parameter and
- * returns view element to use in conversion.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element definition or a function
+ * that takes model marker data as a parameter and returns view ui element.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  */
-export function downcastMarkerToElement( config, priority = 'normal' ) {
+export function downcastMarkerToElement( config ) {
 	config = cloneDeep( config );
 
-	_normalizeToElementConfig( config, ViewUIElement );
+	config.view = _normalizeToElementConfig( config.view, 'ui' );
 
 	return dispatcher => {
-		dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority } );
-		dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority } );
+		dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority: config.priority || 'normal' } );
+		dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority: config.priority || 'normal' } );
 	};
 }
 
@@ -307,7 +292,7 @@ export function downcastMarkerToElement( config, priority = 'normal' ) {
  *
  *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
  *
- *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
+ *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' }, priority: 'high' } );
  *
  * 		downcastMarkerToHighlight( {
  * 			model: 'comment',
@@ -322,9 +307,8 @@ export function downcastMarkerToElement( config, priority = 'normal' ) {
  * 		} );
  *
  * If function is passed as `config.view` parameter, it will be used to generate highlight descriptor. The function
- * receives `data` and `conversionApi` objects as parameters and should return
- * {@link module:engine/conversion/downcast-converters~HighlightDescriptor}. The `data` and `conversionApi` objects are passed from
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
+ * receives `data` object as parameter and should return a {@link module:engine/conversion/downcast-converters~HighlightDescriptor}.
+ * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
@@ -332,139 +316,107 @@ export function downcastMarkerToElement( config, priority = 'normal' ) {
  * @param {String} config.model Name of the model marker (or model marker group) to convert.
  * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view Highlight descriptor
  * which will be used for highlighting or a function that takes model marker data as a parameter and returns a highlight descriptor.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  */
-export function downcastMarkerToHighlight( config, priority = 'normal' ) {
+export function downcastMarkerToHighlight( config ) {
 	return dispatcher => {
-		dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority } );
-		dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority } );
-		dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority } );
+		dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.priority || 'normal' } );
+		dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.priority || 'normal' } );
+		dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority: config.priority || 'normal' } );
 	};
 }
 
-// Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters
-// for generating a view element.
+// Takes `config.view`, and if it is a {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
+// to a function (because lower level converters accepts only element creator functions).
 //
-// @param {Object} config Object with conversion helper configuration.
-// @param {Function} ViewElementClass View element class to use when generating view element from config.
-function _normalizeToElementConfig( config, ViewElementClass ) {
-	// If config is given as an array, normalize each entry separately.
-	if ( Array.isArray( config ) ) {
-		for ( const configEntry of config ) {
-			_normalizeToElementConfig( configEntry, ViewElementClass );
-		}
-
-		return;
+// @param {module:engine/view/elementdefinition~ElementDefinition|Function} view View configuration.
+// @param {'container'|'attribute'|'ui'} viewElementType View element type to create.
+// @returns {Function} Element creator function to use in lower level converters.
+function _normalizeToElementConfig( view, viewElementType ) {
+	if ( typeof view == 'function' ) {
+		// If `view` is already a function, don't do anything.
+		return view;
 	}
 
-	const view = config.view;
-
-	// Build `.view` property.
-	// It is expected to be either string, element definition or creator function.
-	if ( typeof view == 'string' ) {
-		// If `.view` is a string, create a function that returns view element instance out of given `ViewElementClass`.
-		config.view = () => new ViewElementClass( view );
-	} else if ( typeof view == 'object' ) {
-		// If `.view` is an object, use it to build view element instance.
-		const element = _createViewElementFromDefinition( view, ViewElementClass );
-		config.view = () => element.clone();
-	}
-	// `.view` can be also a function that is already valid type which don't have to be normalized.
+	return ( modelData, viewWriter ) => _createViewElementFromDefinition( view, viewWriter, viewElementType );
 }
 
 // Creates view element instance from provided viewElementDefinition and class.
 //
 // @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
-// @param {Function} ViewElementClass
+// @param {module:engine/view/writer~Writer} viewWriter
+// @param {'container'|'attribute'|'ui'} viewElementType
 // @returns {module:engine/view/element~Element}
-function _createViewElementFromDefinition( viewElementDefinition, ViewElementClass ) {
-	const element = new ViewElementClass( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+function _createViewElementFromDefinition( viewElementDefinition, viewWriter, viewElementType ) {
+	if ( typeof viewElementDefinition == 'string' ) {
+		// If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property.
+		viewElementDefinition = { name: viewElementDefinition };
+	}
+
+	let element;
+
+	if ( viewElementType == 'container' ) {
+		element = viewWriter.createContainerElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+	} else if ( viewElementType == 'attribute' ) {
+		element = viewWriter.createAttributeElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+	} else {
+		// 'ui'.
+		element = viewWriter.createUIElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+	}
 
 	if ( viewElementDefinition.style ) {
-		element._setStyle( viewElementDefinition.style );
+		const keys = Object.keys( viewElementDefinition.style );
+
+		for ( const key of keys ) {
+			viewWriter.setStyle( key, viewElementDefinition.style[ key ], element );
+		}
 	}
 
 	if ( viewElementDefinition.class ) {
-		element._addClass( viewElementDefinition.class );
+		const classes = viewElementDefinition.class;
+
+		if ( typeof classes == 'string' ) {
+			viewWriter.addClass( classes, element );
+		} else {
+			for ( const className of classes ) {
+				viewWriter.addClass( className, element );
+			}
+		}
 	}
 
 	return element;
 }
 
-// Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters
-// for generating view attribute.
-//
-// @param {String} modelAttributeKey Model attribute key for which config is defined.
-// @param {Object} [config] Config with conversion helper configuration.
-function _normalizeToAttributeConfig( modelAttributeKey, config ) {
-	// If config is given as an array, normalize each entry separately.
-	if ( Array.isArray( config ) ) {
-		for ( const configEntry of config ) {
-			_normalizeToAttributeConfig( modelAttributeKey, configEntry );
-		}
+function _getFromAttributeCreator( config ) {
+	if ( config.model.values ) {
+		return ( modelAttributeValue, viewWriter ) => {
+			const view = config.view[ modelAttributeValue ];
 
-		return;
-	}
+			if ( view ) {
+				return view( modelAttributeValue, viewWriter );
+			}
 
-	// Build `.view` property.
-	// It is expected to be a creator function, that takes attribute value and model item and returns an object
-	// with `key` property and `value` property which are view attribute key and view attribute value.
-	if ( !config.view ) {
-		// If `.view` is not set, take both attribute name and attribute value from model.
-		const viewAttributeKey = modelAttributeKey;
-		config.view = modelAttributeValue => ( { key: viewAttributeKey, value: modelAttributeValue } );
-	} else if ( typeof config.view == 'string' ) {
-		// If `.view` is set as a string, use it as a view attribute name. Value will be taken from model attribute value.
-		const viewAttributeKey = config.view;
-		config.view = modelAttributeValue => ( { key: viewAttributeKey, value: modelAttributeValue } );
-	} else if ( typeof config.view == 'object' ) {
-		// If `.view` is set as an object, use set key and value.
-		const viewAttributeKey = config.view.key;
-		const viewAttributeValue = config.view.value;
-		config.view = () => ( { key: viewAttributeKey, value: viewAttributeValue } );
+			return null;
+		};
+	} else {
+		return config.view;
 	}
-	// `.view` can be also already a function.
 }
 
-// Takes config and creates a view element creator function that chooses an appropriate entry from the config depending on
-// the value of model attribute.
-//
-// Supports specifying config as a single object or an array of objects.
-// Supports `.view` defined as an object and as a function.
+// Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters
+// for generating view attribute.
 //
-// @param {Object|Array.<Object>} config Config with conversion helper configuration.
-function _getCreatorForArrayConfig( config ) {
-	if ( !Array.isArray( config ) ) {
-		config = [ config ];
+// @param {Object} view View configuration.
+function _normalizeToAttributeConfig( view ) {
+	if ( typeof view == 'string' ) {
+		return modelAttributeValue => ( { key: view, value: modelAttributeValue } );
+	} else if ( typeof view == 'object' ) {
+		return () => view;
+	} else {
+		// function.
+		return view;
 	}
-
-	// Get "default config" entry. It is the entry with `.model` property set to `true`.
-	// "Default" entry should be used if no other entry matched model attribute value.
-	const defaultConfig = config.find( configEntry => configEntry.model === undefined );
-
-	// Return a creator function.
-	return ( modelAttributeValue, data, consumable, conversionApi ) => {
-		// Set default config at first. It will be used if no other entry matches model attribute value.
-		let matchedConfigEntry = defaultConfig;
-
-		// Creator should check all entries from the config...
-		for ( const configEntry of config ) {
-			if ( configEntry.model === modelAttributeValue ) {
-				// If `.model` specified in entry matches converted attribute's value, choose it.
-				matchedConfigEntry = configEntry;
-				break;
-			}
-		}
-
-		// If there was default config or matched config...
-		if ( matchedConfigEntry ) {
-			// The entry `.view` is a function after it got normalized earlier, execute it and return the value.
-			return matchedConfigEntry.view( modelAttributeValue, data, consumable, conversionApi );
-		}
-
-		return null;
-	};
 }
 
 /**
@@ -495,7 +447,7 @@ function _getCreatorForArrayConfig( config ) {
  */
 export function insertElement( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
-		const viewElement = elementCreator( data.item, consumable, conversionApi );
+		const viewElement = elementCreator( data.item, conversionApi.writer );
 
 		if ( !viewElement ) {
 			return;
@@ -579,20 +531,13 @@ export function remove() {
  */
 export function insertUIElement( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
-		let viewStartElement, viewEndElement;
-
 		// Create two view elements. One will be inserted at the beginning of marker, one at the end.
 		// If marker is collapsed, only "opening" element will be inserted.
-		if ( elementCreator instanceof ViewElement ) {
-			viewStartElement = elementCreator.clone( true );
-			viewEndElement = elementCreator.clone( true );
-		} else {
-			data.isOpening = true;
-			viewStartElement = elementCreator( data, conversionApi );
+		data.isOpening = true;
+		const viewStartElement = elementCreator( data, conversionApi.writer );
 
-			data.isOpening = false;
-			viewEndElement = elementCreator( data, conversionApi );
-		}
+		data.isOpening = false;
+		const viewEndElement = elementCreator( data, conversionApi.writer );
 
 		if ( !viewStartElement || !viewEndElement ) {
 			return;
@@ -639,20 +584,13 @@ export function insertUIElement( elementCreator ) {
  */
 export function removeUIElement( elementCreator ) {
 	return ( evt, data, conversionApi ) => {
-		let viewStartElement, viewEndElement;
-
 		// Create two view elements. One will be used to remove "opening element", the other for "closing element".
 		// If marker was collapsed, only "opening" element will be removed.
-		if ( elementCreator instanceof ViewElement ) {
-			viewStartElement = elementCreator.clone( true );
-			viewEndElement = elementCreator.clone( true );
-		} else {
-			data.isOpening = true;
-			viewStartElement = elementCreator( data, conversionApi );
+		data.isOpening = true;
+		const viewStartElement = elementCreator( data, conversionApi.writer );
 
-			data.isOpening = false;
-			viewEndElement = elementCreator( data, conversionApi );
-		}
+		data.isOpening = false;
+		const viewEndElement = elementCreator( data, conversionApi.writer );
 
 		if ( !viewStartElement || !viewEndElement ) {
 			return;
@@ -702,8 +640,7 @@ export function removeUIElement( elementCreator ) {
  *
  * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
  * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
- * The function is passed all the parameters of the
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute} event.
+ * The function is passed model attribute value as first parameter and additional data about the change as a second parameter.
  * @returns {Function} Set/change attribute converter.
  */
 export function changeAttribute( attributeCreator ) {
@@ -714,22 +651,49 @@ export function changeAttribute( attributeCreator ) {
 			return;
 		}
 
-		// First remove the old attribute if there was one.
-		const oldAttribute = attributeCreator( data.attributeOldValue, data, consumable, conversionApi );
-		const mapper = conversionApi.mapper;
+		const viewElement = conversionApi.mapper.toViewElement( data.item );
 		const viewWriter = conversionApi.writer;
 
+		// First remove the old attribute if there was one.
+		const oldAttribute = attributeCreator( data.attributeOldValue, data );
+
 		if ( data.attributeOldValue !== null && oldAttribute ) {
-			const viewElement = mapper.toViewElement( data.item );
-			viewWriter.removeAttribute( oldAttribute.key, viewElement );
+			if ( oldAttribute.key == 'class' ) {
+				const classes = Array.isArray( oldAttribute.value ) ? oldAttribute.value : [ oldAttribute.value ];
+
+				for ( const className of classes ) {
+					viewWriter.removeClass( className, viewElement );
+				}
+			} else if ( oldAttribute.key == 'style' ) {
+				const keys = Object.keys( oldAttribute.value );
+
+				for ( const key of keys ) {
+					viewWriter.removeStyle( key, viewElement );
+				}
+			} else {
+				viewWriter.removeAttribute( oldAttribute.key, viewElement );
+			}
 		}
 
 		// Then, if conversion was successful, set the new attribute.
 		const newAttribute = attributeCreator( data.attributeNewValue, data, consumable, conversionApi );
 
 		if ( data.attributeNewValue !== null && newAttribute ) {
-			const viewElement = mapper.toViewElement( data.item );
-			viewWriter.setAttribute( newAttribute.key, newAttribute.value, viewElement );
+			if ( newAttribute.key == 'class' ) {
+				const classes = Array.isArray( newAttribute.value ) ? newAttribute.value : [ newAttribute.value ];
+
+				for ( const className of classes ) {
+					viewWriter.addClass( className, viewElement );
+				}
+			} else if ( newAttribute.key == 'style' ) {
+				const keys = Object.keys( newAttribute.value );
+
+				for ( const key of keys ) {
+					viewWriter.setStyle( key, newAttribute.value[ key ], viewElement );
+				}
+			} else {
+				viewWriter.setAttribute( newAttribute.key, newAttribute.value, viewElement );
+			}
 		}
 	};
 }
@@ -768,10 +732,10 @@ export function wrap( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
 		// Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
 		// or the attribute was removed.
-		const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi );
+		const oldViewElement = elementCreator( data.attributeOldValue, conversionApi.writer );
 
 		// Create node to wrap with.
-		const newViewElement = elementCreator( data.attributeNewValue, data, consumable, conversionApi );
+		const newViewElement = elementCreator( data.attributeNewValue, conversionApi.writer );
 
 		if ( !oldViewElement && !newViewElement ) {
 			return;

+ 33 - 25
packages/ckeditor5-engine/src/conversion/upcast-converters.js

@@ -26,7 +26,7 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  *
  *		upcastElementToElement( { view: 'p', model: 'paragraph' } );
  *
- *		upcastElementToElement( { view: 'p', model: 'paragraph' }, 'high' );
+ *		upcastElementToElement( { view: 'p', model: 'paragraph', priority: 'high' } );
  *
  *		upcastElementToElement( {
  *			view: {
@@ -52,10 +52,10 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element
  * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  */
-export function upcastElementToElement( config, priority = 'normal' ) {
+export function upcastElementToElement( config ) {
 	config = cloneDeep( config );
 
 	const converter = _prepareToElementConverter( config );
@@ -64,7 +64,7 @@ export function upcastElementToElement( config, priority = 'normal' ) {
 	const eventName = elementName ? 'element:' + elementName : 'element';
 
 	return dispatcher => {
-		dispatcher.on( eventName, converter, { priority } );
+		dispatcher.on( eventName, converter, { priority: config.priority || 'normal' } );
 	};
 }
 
@@ -78,7 +78,7 @@ export function upcastElementToElement( config, priority = 'normal' ) {
  *
  *		upcastElementToAttribute( { view: 'strong', model: 'bold' } );
  *
- *		upcastElementToAttribute( { view: 'strong', model: 'bold' }, 'normal' );
+ *		upcastElementToAttribute( { view: 'strong', model: 'bold', priority: 'high' } );
  *
  *		upcastElementToAttribute( {
  *			view: {
@@ -130,10 +130,10 @@ export function upcastElementToElement( config, priority = 'normal' ) {
  * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
  * the model attribute. `value` property may be set as a function that takes a view element and returns the value.
  * If `String` is given, the model attribute value will be set to `true`.
- * @param {module:utils/priorities~PriorityString} [priority='low'] Converter priority.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  */
-export function upcastElementToAttribute( config, priority = 'low' ) {
+export function upcastElementToAttribute( config ) {
 	config = cloneDeep( config );
 
 	_normalizeModelAttributeConfig( config );
@@ -144,7 +144,7 @@ export function upcastElementToAttribute( config, priority = 'low' ) {
 	const eventName = elementName ? 'element:' + elementName : 'element';
 
 	return dispatcher => {
-		dispatcher.on( eventName, converter, { priority } );
+		dispatcher.on( eventName, converter, { priority: config.priority || 'normal' } );
 	};
 }
 
@@ -160,7 +160,7 @@ export function upcastElementToAttribute( config, priority = 'low' ) {
  *
  *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } );
  *
- *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
+ *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', priority: 'normal' } );
  *
  *		upcastAttributeToAttribute( {
  *			view: {
@@ -172,7 +172,7 @@ export function upcastElementToAttribute( config, priority = 'low' ) {
  *
  *		upcastAttributeToAttribute( {
  *			view: {
- *				name: 'span',
+ *				name: 'img',
  *				key: 'class',
  *				value: 'styled-dark'
  *			},
@@ -209,10 +209,10 @@ export function upcastElementToAttribute( config, priority = 'low' ) {
  * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
  * the model attribute. `value` property may be set as a function that takes a view element and returns the value.
  * If `String` is given, the model attribute value will be same as view attribute value.
- * @param {module:utils/priorities~PriorityString} [priority='low'] Converter priority.
+ * @param {module:utils/priorities~PriorityString} [config.priority='low'] Converter priority.
  * @returns {Function} Conversion helper.
  */
-export function upcastAttributeToAttribute( config, priority = 'low' ) {
+export function upcastAttributeToAttribute( config ) {
 	config = cloneDeep( config );
 
 	let viewKey = null;
@@ -226,7 +226,7 @@ export function upcastAttributeToAttribute( config, priority = 'low' ) {
 	const converter = _prepareToAttributeConverter( config, false );
 
 	return dispatcher => {
-		dispatcher.on( 'element', converter, { priority } );
+		dispatcher.on( 'element', converter, { priority: config.priority || 'low' } );
 	};
 }
 
@@ -240,7 +240,7 @@ export function upcastAttributeToAttribute( config, priority = 'low' ) {
  *
  *		upcastElementToMarker( { view: 'marker-search', model: 'search' } );
  *
- *		upcastElementToMarker( { view: 'marker-search', model: 'search' }, 'high' );
+ *		upcastElementToMarker( { view: 'marker-search', model: 'search', priority: 'high' } );
  *
  *		upcastElementToMarker( {
  *			view: 'marker-search',
@@ -263,15 +263,15 @@ export function upcastAttributeToAttribute( config, priority = 'low' ) {
  * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns
  * a model marker name.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  */
-export function upcastElementToMarker( config, priority = 'normal' ) {
+export function upcastElementToMarker( config ) {
 	config = cloneDeep( config );
 
 	_normalizeToMarkerConfig( config );
 
-	return upcastElementToElement( config, priority );
+	return upcastElementToElement( config );
 }
 
 // Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name
@@ -391,13 +391,21 @@ function _normalizeViewAttributeKeyValueConfig( config ) {
 	}
 
 	const key = config.view.key;
-	const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
+	let normalized;
 
-	const normalized = {
-		attribute: {
-			[ key ]: value
-		}
-	};
+	if ( key == 'class' || key == 'style' ) {
+		normalized = {
+			[ key ]: config.view.value
+		};
+	} else {
+		const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
+
+		normalized = {
+			attribute: {
+				[ key ]: value
+			}
+		};
+	}
 
 	if ( config.view.name ) {
 		normalized.name = config.view.name;
@@ -504,10 +512,10 @@ function _setAttributeOn( modelRange, modelAttribute, conversionApi ) {
 function _normalizeToMarkerConfig( config ) {
 	const oldModel = config.model;
 
-	config.model = ( viewElement, writer ) => {
+	config.model = ( viewElement, modelWriter ) => {
 		const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement );
 
-		return writer.createElement( '$marker', { 'data-name': markerName } );
+		return modelWriter.createElement( '$marker', { 'data-name': markerName } );
 	};
 }
 

+ 15 - 7
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -16,24 +16,25 @@ import Model from '../model/model';
 import Batch from '../model/batch';
 import ModelRange from '../model/range';
 import ModelPosition from '../model/position';
-import DowncastDispatcher from '../conversion/downcastdispatcher';
 import ModelSelection from '../model/selection';
 import ModelDocumentFragment from '../model/documentfragment';
 import DocumentSelection from '../model/documentselection';
 
 import View from '../view/view';
-import UpcastDispatcher from '../conversion/upcastdispatcher';
 import ViewContainerElement from '../view/containerelement';
-import ViewAttributeElement from '../view/attributeelement';
 import ViewRootEditableElement from '../view/rooteditableelement';
 
-import Mapper from '../conversion/mapper';
 import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view';
+
+import DowncastDispatcher from '../conversion/downcastdispatcher';
+import UpcastDispatcher from '../conversion/upcastdispatcher';
+import Mapper from '../conversion/mapper';
 import {
 	convertRangeSelection,
 	convertCollapsedSelection,
 } from '../conversion/downcast-selection-converters';
 import { insertText, insertElement, wrap } from '../conversion/downcast-converters';
+
 import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObject';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 
@@ -208,11 +209,18 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	mapper.bindElements( node.root, viewRoot );
 
 	downcastDispatcher.on( 'insert:$text', insertText() );
-	downcastDispatcher.on( 'attribute', wrap( ( value, data ) => {
+	downcastDispatcher.on( 'attribute', ( evt, data, consumable, conversionApi ) => {
 		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( 'textProxy' ) ) {
-			return new ViewAttributeElement( 'model-text-with-attributes', { [ data.attributeKey ]: stringifyAttributeValue( value ) } );
+			const converter = wrap( ( modelAttributeValue, viewWriter ) => {
+				return viewWriter.createAttributeElement(
+					'model-text-with-attributes',
+					{ [ data.attributeKey ]: stringifyAttributeValue( modelAttributeValue ) }
+				);
+			} );
+
+			converter( evt, data, consumable, conversionApi );
 		}
-	} ) );
+	} );
 	downcastDispatcher.on( 'insert', insertElement( modelItem => {
 		// Stringify object types values for properly display as an output string.
 		const attributes = convertAttributes( modelItem.getAttributes(), stringifyAttributeValue );

+ 2 - 2
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -264,7 +264,7 @@ describe( 'DataController', () => {
 			setData( model, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
 			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
-			downcastAttributeToElement( 'bold', { view: 'strong' } )( data.downcastDispatcher );
+			downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher );
 
 			expect( data.get() ).to.equal( '<p>foo<strong>bar</strong></p>' );
 		} );
@@ -277,7 +277,7 @@ describe( 'DataController', () => {
 			setData( model, 'Bar', { rootName: 'title' } );
 
 			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
-			downcastAttributeToElement( 'bold', { view: 'strong' } )( data.downcastDispatcher );
+			downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher );
 
 			expect( data.get() ).to.equal( '<p>foo</p>' );
 			expect( data.get( 'main' ) ).to.equal( '<p>foo</p>' );

+ 95 - 130
packages/ckeditor5-engine/tests/conversion/conversion.js

@@ -93,7 +93,7 @@ describe( 'Conversion', () => {
 			const modelDoc = model.document;
 			modelRoot = modelDoc.createRoot();
 
-			viewRoot = controller.view.getRoot();
+			viewRoot = controller.view.document.getRoot();
 			// Set name of view root the same as dom root.
 			// This is a mock of attaching view root to dom root.
 			viewRoot._name = 'div';
@@ -125,6 +125,14 @@ describe( 'Conversion', () => {
 				test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
 			} );
 
+			it( 'config.priority is defined', () => {
+				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+				conversion.elementToElement( { model: 'paragraph', view: 'div', priority: 'high' } );
+
+				test( '<div>Foo</div>', '<paragraph>Foo</paragraph>' );
+				test( '<p>Foo</p>', '<paragraph>Foo</paragraph>', '<div>Foo</div>' );
+			} );
+
 			it( 'config.view is an object', () => {
 				schema.register( 'fancyParagraph', {
 					inheritAllFrom: 'paragraph'
@@ -148,7 +156,7 @@ describe( 'Conversion', () => {
 					upcastAlso: [
 						'div',
 						{
-							// Match any name.
+							// Any element with `display: block` style.
 							name: /./,
 							style: {
 								display: 'block'
@@ -212,13 +220,22 @@ describe( 'Conversion', () => {
 			} );
 
 			it( 'config.view is a string', () => {
-				conversion.attributeToElement( 'bold', { view: 'strong' } );
+				conversion.attributeToElement( { model: 'bold', view: 'strong' } );
 
 				test( '<p><strong>Foo</strong> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
 			} );
 
+			it( 'config.priority is defined', () => {
+				conversion.attributeToElement( { model: 'bold', view: 'strong' } );
+				conversion.attributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
+
+				test( '<p><b>Foo</b></p>', '<paragraph><$text bold="true">Foo</$text></paragraph>' );
+				test( '<p><strong>Foo</strong></p>', '<paragraph><$text bold="true">Foo</$text></paragraph>', '<p><b>Foo</b></p>' );
+			} );
+
 			it( 'config.view is an object', () => {
-				conversion.attributeToElement( 'bold', {
+				conversion.attributeToElement( {
+					model: 'bold',
 					view: {
 						name: 'span',
 						class: 'bold'
@@ -229,7 +246,8 @@ describe( 'Conversion', () => {
 			} );
 
 			it( 'config.view is an object with upcastAlso defined', () => {
-				conversion.attributeToElement( 'bold', {
+				conversion.attributeToElement( {
+					model: 'bold',
 					view: 'strong',
 					upcastAlso: [
 						'b',
@@ -292,77 +310,32 @@ describe( 'Conversion', () => {
 				);
 			} );
 
-			it( 'config.model is a string', () => {
-				schema.extend( '$text', {
-					allowAttributes: [ 'styled' ]
-				} );
-
-				conversion.attributeToElement( 'styled', {
-					model: 'dark',
-					view: {
-						name: 'span',
-						class: [ 'styled', 'styled-dark' ]
-					}
-				} );
-
-				test(
-					'<p><span class="styled styled-dark">Foo</span> bar</p>',
-					'<paragraph><$text styled="dark">Foo</$text> bar</paragraph>'
-				);
-			} );
-
-			it( 'config is an array', () => {
+			it( 'model attribute value is enumerable', () => {
 				schema.extend( '$text', {
 					allowAttributes: [ 'fontSize' ]
 				} );
 
-				conversion.attributeToElement( 'fontSize', [
-					{
-						model: 'big',
-						view: {
+				conversion.attributeToElement( {
+					model: {
+						key: 'fontSize',
+						values: [ 'big', 'small' ]
+					},
+					view: {
+						big: {
 							name: 'span',
 							style: {
 								'font-size': '1.2em'
 							}
-						}
-					},
-					{
-						model: 'small',
-						view: {
+						},
+						small: {
 							name: 'span',
 							style: {
 								'font-size': '0.8em'
 							}
 						}
-					}
-				] );
-
-				test(
-					'<p><span style="font-size:1.2em">Foo</span> bar</p>',
-					'<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>'
-				);
-
-				test(
-					'<p><span style="font-size:0.8em">Foo</span> bar</p>',
-					'<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>'
-				);
-			} );
-
-			it( 'config is an array with upcastAlso defined', () => {
-				schema.extend( '$text', {
-					allowAttributes: [ 'fontSize' ]
-				} );
-
-				conversion.attributeToElement( 'fontSize', [
-					{
-						model: 'big',
-						view: {
-							name: 'span',
-							style: {
-								'font-size': '1.2em'
-							}
-						},
-						upcastAlso: viewElement => {
+					},
+					upcastAlso: {
+						big: viewElement => {
 							const fontSize = viewElement.getStyle( 'font-size' );
 
 							if ( !fontSize ) {
@@ -382,17 +355,8 @@ describe( 'Conversion', () => {
 							}
 
 							return null;
-						}
-					},
-					{
-						model: 'small',
-						view: {
-							name: 'span',
-							style: {
-								'font-size': '0.8em'
-							}
 						},
-						upcastAlso: viewElement => {
+						small: viewElement => {
 							const fontSize = viewElement.getStyle( 'font-size' );
 
 							if ( !fontSize ) {
@@ -414,7 +378,7 @@ describe( 'Conversion', () => {
 							return null;
 						}
 					}
-				] );
+				} );
 
 				test(
 					'<p><span style="font-size:1.2em">Foo</span> bar</p>',
@@ -455,70 +419,70 @@ describe( 'Conversion', () => {
 				} );
 			} );
 
-			it( 'config is not set', () => {
-				schema.extend( 'image', {
-					allowAttributes: [ 'src' ]
-				} );
-
-				conversion.attributeToAttribute( 'src' );
-
-				test( '<img src="foo.jpg"></img>', '<image src="foo.jpg"></image>' );
-			} );
-
-			it( 'config.view is a string', () => {
+			it( 'config.view and config.model are strings', () => {
 				schema.extend( 'image', {
 					allowAttributes: [ 'source' ]
 				} );
 
-				conversion.attributeToAttribute( 'source', { view: 'src' } );
+				conversion.attributeToAttribute( { model: 'source', view: 'src' } );
 
 				test( '<img src="foo.jpg"></img>', '<image source="foo.jpg"></image>' );
 			} );
 
-			it( 'config.view is an object', () => {
+			it( 'config.view and config.model are objects', () => {
 				schema.extend( 'image', {
 					allowAttributes: [ 'aside' ]
 				} );
 
-				conversion.attributeToAttribute( 'aside', {
-					model: true,
+				conversion.attributeToAttribute( {
+					model: {
+						name: 'image',
+						key: 'aside',
+						values: [ 'aside' ]
+					},
 					view: {
-						name: 'img',
-						key: 'class',
-						value: 'aside half-size'
+						aside: {
+							name: 'img',
+							key: 'class',
+							value: [ 'aside', 'half-size' ]
+						}
 					}
 				} );
 
 				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 
-				test( '<img class="aside half-size"></img>', '<image aside="true"></image>' );
+				test( '<img class="aside half-size"></img>', '<image aside="aside"></image>' );
 				test( '<p class="aside half-size"></p>', '<paragraph></paragraph>', '<p></p>' );
 			} );
 
-			it( 'config is an array', () => {
+			it( 'config.view and config.model are objects - convert to style attribute', () => {
 				schema.extend( 'image', {
-					allowAttributes: [ 'styled' ]
+					allowAttributes: [ 'aside' ]
 				} );
 
-				conversion.attributeToAttribute( 'styled', [
-					{
-						model: 'dark',
-						view: {
-							key: 'class',
-							value: 'styled styled-dark'
-						}
+				conversion.attributeToAttribute( {
+					model: {
+						name: 'image',
+						key: 'aside',
+						values: [ 'aside' ]
 					},
-					{
-						model: 'light',
-						view: {
-							key: 'class',
-							value: 'styled styled-light'
+					view: {
+						aside: {
+							name: 'img',
+							key: 'style',
+							value: {
+								float: 'right',
+								width: '50%',
+								margin: '5px'
+							}
 						}
 					}
-				] );
+				} );
+
+				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 
-				test( '<img class="styled styled-dark"></img>', '<image styled="dark"></image>' );
-				test( '<img class="styled styled-light"></img>', '<image styled="light"></image>' );
+				test( '<img style="float:right;margin:5px;width:50%"></img>', '<image aside="aside"></image>' );
+				test( '<p style="float:right;margin:5px;width:50%"></p>', '<paragraph></paragraph>', '<p></p>' );
 			} );
 
 			it( 'config is an array with upcastAlso defined', () => {
@@ -528,36 +492,34 @@ describe( 'Conversion', () => {
 					allowAttributes: [ 'align' ]
 				} );
 
-				conversion.attributeToAttribute( 'align', [
-					{
-						model: 'right',
-						view: {
+				conversion.attributeToAttribute( {
+					model: {
+						key: 'align',
+						values: [ 'right', 'center' ]
+					},
+					view: {
+						right: {
 							key: 'class',
 							value: 'align-right'
 						},
-						upcastAlso: viewElement => {
-							if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
-								return {
-									style: [ 'text-align' ]
-								};
-							}
-
-							return null;
-						}
-					},
-					{
-						model: 'center',
-						view: {
+						center: {
 							key: 'class',
 							value: 'align-center'
+						}
+					},
+					upcastAlso: {
+						right: {
+							style: {
+								'text-align': 'right'
+							}
 						},
-						upcastAlso: {
+						center: {
 							style: {
 								'text-align': 'center'
 							}
 						}
 					}
-				] );
+				} );
 
 				test(
 					'<p class="align-right">Foo</p>',
@@ -592,8 +554,11 @@ describe( 'Conversion', () => {
 
 		function loadData( input ) {
 			const parsedView = viewParse( input );
+			let convertedModel;
 
-			const convertedModel = viewDispatcher.convert( parsedView );
+			model.change( writer => {
+				convertedModel = viewDispatcher.convert( parsedView, writer );
+			} );
 
 			model.change( writer => {
 				writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, modelRoot.maxOffset ) );

+ 164 - 170
packages/ckeditor5-engine/tests/conversion/downcast-converters.js

@@ -15,7 +15,6 @@ import ModelPosition from '../../src/model/position';
 
 import ViewElement from '../../src/view/element';
 import ViewContainerElement from '../../src/view/containerelement';
-import ViewAttributeElement from '../../src/view/attributeelement';
 import ViewUIElement from '../../src/view/uielement';
 import ViewText from '../../src/view/text';
 
@@ -62,7 +61,7 @@ describe( 'downcast-helpers', () => {
 
 		it( 'can be overwritten using priority', () => {
 			const helperA = downcastElementToElement( { model: 'paragraph', view: 'p' } );
-			const helperB = downcastElementToElement( { model: 'paragraph', view: 'foo' }, 'high' );
+			const helperB = downcastElementToElement( { model: 'paragraph', view: 'foo', priority: 'high' } );
 
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 
@@ -94,7 +93,7 @@ describe( 'downcast-helpers', () => {
 		it( 'config.view is a function', () => {
 			const helper = downcastElementToElement( {
 				model: 'heading',
-				view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
+				view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
 			} );
 
 			conversion.for( 'downcast' ).add( helper );
@@ -109,7 +108,7 @@ describe( 'downcast-helpers', () => {
 
 	describe( 'downcastAttributeToElement', () => {
 		it( 'config.view is a string', () => {
-			const helper = downcastAttributeToElement( 'bold', { view: 'strong' } );
+			const helper = downcastAttributeToElement( { model: 'bold', view: 'strong' } );
 
 			conversion.for( 'downcast' ).add( helper );
 
@@ -121,8 +120,8 @@ describe( 'downcast-helpers', () => {
 		} );
 
 		it( 'can be overwritten using priority', () => {
-			const helperA = downcastAttributeToElement( 'bold', { view: 'strong' } );
-			const helperB = downcastAttributeToElement( 'bold', { view: 'b' }, 'high' );
+			const helperA = downcastAttributeToElement( { model: 'bold', view: 'strong' } );
+			const helperB = downcastAttributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
 
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 
@@ -134,67 +133,44 @@ describe( 'downcast-helpers', () => {
 		} );
 
 		it( 'config.view is a view element definition', () => {
-			const helper = downcastAttributeToElement( 'bold', {
+			const helper = downcastAttributeToElement( {
+				model: 'invert',
 				view: {
 					name: 'span',
-					class: 'bold'
+					class: [ 'font-light', 'bg-dark' ]
 				}
 			} );
 
 			conversion.for( 'downcast' ).add( helper );
 
 			model.change( writer => {
-				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
+				writer.insertText( 'foo', { invert: true }, modelRoot, 0 );
 			} );
 
-			expectResult( '<span class="bold">foo</span>' );
+			expectResult( '<span class="bg-dark font-light">foo</span>' );
 		} );
 
-		it( 'config.view is a view element definition, model attribute value specified', () => {
-			const helper = downcastAttributeToElement( 'styled', {
-				model: 'dark',
+		it( 'model attribute value is enum', () => {
+			const helper = downcastAttributeToElement( {
+				model: {
+					key: 'fontSize',
+					values: [ 'big', 'small' ]
+				},
 				view: {
-					name: 'span',
-					class: [ 'styled', 'styled-dark' ]
-				}
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', { styled: 'dark' }, modelRoot, 0 );
-			} );
-
-			expectResult( '<span class="styled styled-dark">foo</span>' );
-
-			model.change( writer => {
-				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
-			} );
-
-			expectResult( 'foo' );
-		} );
-
-		it( 'multiple config items', () => {
-			const helper = downcastAttributeToElement( 'fontSize', [
-				{
-					model: 'big',
-					view: {
+					big: {
 						name: 'span',
 						style: {
 							'font-size': '1.2em'
 						}
-					}
-				},
-				{
-					model: 'small',
-					view: {
+					},
+					small: {
 						name: 'span',
 						style: {
 							'font-size': '0.8em'
 						}
 					}
 				}
-			] );
+			} );
 
 			conversion.for( 'downcast' ).add( helper );
 
@@ -218,8 +194,11 @@ describe( 'downcast-helpers', () => {
 		} );
 
 		it( 'config.view is a function', () => {
-			const helper = downcastAttributeToElement( 'bold', {
-				view: attributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + attributeValue } )
+			const helper = downcastAttributeToElement( {
+				model: 'bold',
+				view: ( modelAttributeValue, viewWriter ) => {
+					return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
+				}
 			} );
 
 			conversion.for( 'downcast' ).add( helper );
@@ -237,33 +216,27 @@ describe( 'downcast-helpers', () => {
 			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'image', view: 'img' } ) );
 		} );
 
-		it( 'config not set', () => {
-			const helper = downcastAttributeToAttribute( 'src' );
+		it( 'config.view is a string', () => {
+			const helper = downcastAttributeToAttribute( { model: 'source', view: 'src' } );
 
 			conversion.for( 'downcast' ).add( helper );
 
 			model.change( writer => {
-				writer.insertElement( 'image', { src: 'foo.jpg' }, modelRoot, 0 );
+				writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
 			} );
 
 			expectResult( '<img src="foo.jpg"></img>' );
-		} );
-
-		it( 'config.view is a string', () => {
-			const helper = downcastAttributeToAttribute( 'source', { view: 'src' } );
-
-			conversion.for( 'downcast' ).add( helper );
 
 			model.change( writer => {
-				writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
+				writer.removeAttribute( 'source', modelRoot.getChild( 0 ) );
 			} );
 
-			expectResult( '<img src="foo.jpg"></img>' );
+			expectResult( '<img></img>' );
 		} );
 
 		it( 'can be overwritten using priority', () => {
-			const helperA = downcastAttributeToAttribute( 'source', { view: 'href' } );
-			const helperB = downcastAttributeToAttribute( 'source', { view: 'src' }, 'high' );
+			const helperA = downcastAttributeToAttribute( { model: 'source', view: 'href' } );
+			const helperB = downcastAttributeToAttribute( { model: 'source', view: 'src', priority: 'high' } );
 
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 
@@ -274,83 +247,121 @@ describe( 'downcast-helpers', () => {
 			expectResult( '<img src="foo.jpg"></img>' );
 		} );
 
-		it( 'config.view is an object', () => {
-			const helper = downcastAttributeToAttribute( 'stylish', { view: { key: 'class', value: 'styled' } } );
+		it( 'model element name specified', () => {
+			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) );
+
+			const helper = downcastAttributeToAttribute( {
+				model: {
+					name: 'image',
+					key: 'source'
+				},
+				view: 'src'
+			} );
 
 			conversion.for( 'downcast' ).add( helper );
 
 			model.change( writer => {
-				writer.insertElement( 'image', { stylish: true }, modelRoot, 0 );
+				writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
 			} );
 
-			expectResult( '<img class="styled"></img>' );
+			expectResult( '<img src="foo.jpg"></img>' );
+
+			model.change( writer => {
+				writer.rename( modelRoot.getChild( 0 ), 'paragraph' );
+			} );
+
+			expectResult( '<p></p>' );
 		} );
 
-		it( 'config.view is an object, model attribute value specified', () => {
-			const helper = downcastAttributeToAttribute( 'styled', {
-				model: 'dark',
+		it( 'config.view is an object, model attribute value is enum', () => {
+			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) );
+
+			const helper = downcastAttributeToAttribute( {
+				model: {
+					key: 'styled',
+					values: [ 'dark', 'light' ]
+				},
 				view: {
-					key: 'class',
-					value: 'styled-dark styled'
+					dark: {
+						key: 'class',
+						value: [ 'styled', 'styled-dark' ]
+					},
+					light: {
+						key: 'class',
+						value: [ 'styled', 'styled-light' ]
+					}
 				}
 			} );
 
 			conversion.for( 'downcast' ).add( helper );
 
 			model.change( writer => {
-				writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
+				writer.insertElement( 'paragraph', { styled: 'dark' }, modelRoot, 0 );
 			} );
 
-			expectResult( '<img class="styled styled-dark"></img>' );
+			expectResult( '<p class="styled styled-dark"></p>' );
 
 			model.change( writer => {
-				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
+				writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
 			} );
 
-			expectResult( '<img></img>' );
+			expectResult( '<p class="styled styled-light"></p>' );
+
+			model.change( writer => {
+				writer.removeAttribute( 'styled', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( '<p></p>' );
 		} );
 
-		it( 'multiple config items', () => {
-			const helper = downcastAttributeToAttribute( 'styled', [
-				{
-					model: 'dark',
-					view: {
-						key: 'class',
-						value: 'styled-dark'
-					}
+		it( 'config.view is an object, model attribute value is enum, view has style', () => {
+			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) );
+
+			const helper = downcastAttributeToAttribute( {
+				model: {
+					key: 'align',
+					values: [ 'right', 'center' ]
 				},
-				{
-					model: 'light',
-					view: {
-						key: 'class',
-						value: 'styled-light'
+				view: {
+					right: {
+						key: 'style',
+						value: {
+							'text-align': 'right'
+						}
+					},
+					center: {
+						key: 'style',
+						value: {
+							'text-align': 'center'
+						}
 					}
 				}
-			] );
+			} );
 
 			conversion.for( 'downcast' ).add( helper );
 
 			model.change( writer => {
-				writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
+				writer.insertElement( 'paragraph', { align: 'right' }, modelRoot, 0 );
 			} );
 
-			expectResult( '<img class="styled-dark"></img>' );
+			expectResult( '<p style="text-align:right"></p>' );
 
 			model.change( writer => {
-				writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
+				writer.setAttribute( 'align', 'center', modelRoot.getChild( 0 ) );
 			} );
 
-			expectResult( '<img class="styled-light"></img>' );
+			expectResult( '<p style="text-align:center"></p>' );
 
 			model.change( writer => {
-				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
+				writer.removeAttribute( 'align', modelRoot.getChild( 0 ) );
 			} );
 
-			expectResult( '<img></img>' );
+			expectResult( '<p></p>' );
 		} );
 
 		it( 'config.view is a function', () => {
-			const helper = downcastAttributeToAttribute( 'styled', {
+			const helper = downcastAttributeToAttribute( {
+				model: 'styled',
 				view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } )
 			} );
 
@@ -380,7 +391,7 @@ describe( 'downcast-helpers', () => {
 
 		it( 'can be overwritten using priority', () => {
 			const helperA = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
-			const helperB = downcastMarkerToElement( { model: 'search', view: 'search' }, 'high' );
+			const helperB = downcastMarkerToElement( { model: 'search', view: 'search', priority: 'high' } );
 
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 
@@ -416,8 +427,8 @@ describe( 'downcast-helpers', () => {
 		it( 'config.view is a function', () => {
 			const helper = downcastMarkerToElement( {
 				model: 'search',
-				view: data => {
-					return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
+				view: ( data, viewWriter ) => {
+					return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
 				}
 			} );
 
@@ -448,7 +459,7 @@ describe( 'downcast-helpers', () => {
 
 		it( 'can be overwritten using priority', () => {
 			const helperA = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
-			const helperB = downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
+			const helperB = downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' }, priority: 'high' } );
 
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 
@@ -508,7 +519,7 @@ describe( 'downcast-converters', () => {
 		dispatcher.on(
 			'insert:paragraph',
 			insertElement(
-				( modelItem, consumable, conversionApi ) => conversionApi.writer.createContainerElement( 'p' )
+				( modelItem, viewWriter ) => viewWriter.createContainerElement( 'p' )
 			)
 		);
 
@@ -581,7 +592,7 @@ describe( 'downcast-converters', () => {
 	} );
 
 	describe( 'insertElement', () => {
-		it( 'should convert element insertion in model to and map positions for future converting', () => {
+		it( 'should convert element insertion in model', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
 
 			model.change( writer => {
@@ -591,26 +602,16 @@ describe( 'downcast-converters', () => {
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 		} );
 
-		it( 'should take view element function generator as a parameter', () => {
-			const elementGenerator = ( modelItem, consumable ) => {
-				if ( consumable.consume( modelItem, 'attribute:nice' ) ) {
-					return new ViewContainerElement( 'div' );
-				}
-
-				// Test if default converter will be fired for paragraph, if `null` is returned and consumable was not consumed.
-				return null;
-			};
-
-			dispatcher.on( 'insert:paragraph', insertElement( elementGenerator ), { priority: 'high' } );
+		it( 'should not convert if creator returned null', () => {
+			dispatcher.on( 'insert:div', insertElement( () => null ) );
 
-			const niceP = new ModelElement( 'paragraph', { nice: true }, new ModelText( 'foo' ) );
-			const badP = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) );
+			const modelElement = new ModelElement( 'div' );
 
 			model.change( writer => {
-				writer.insert( [ niceP, badP ], modelRootStart );
+				writer.insert( modelElement, modelRootStart );
 			} );
 
-			expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div><p>bar</p></div>' );
+			expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
 		} );
 	} );
 
@@ -646,7 +647,7 @@ describe( 'downcast-converters', () => {
 				return { key: 'class', value };
 			};
 
-			dispatcher.on( 'insert:div', insertElement( ( model, consumable, api ) => api.writer.createContainerElement( 'div' ) ) );
+			dispatcher.on( 'insert:div', insertElement( ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'div' ) ) );
 			dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) );
 
 			const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
@@ -690,7 +691,7 @@ describe( 'downcast-converters', () => {
 	describe( 'wrap', () => {
 		it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
-			const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' );
+			const creator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' );
 
 			dispatcher.on( 'attribute:bold', wrap( creator ) );
 
@@ -710,9 +711,9 @@ describe( 'downcast-converters', () => {
 		it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
 
-			const elementGenerator = ( value, data, consumable, api ) => {
-				if ( value == 'bold' ) {
-					return api.writer.createAttributeElement( 'b' );
+			const elementGenerator = ( modelAttributeValue, viewWriter ) => {
+				if ( modelAttributeValue == 'bold' ) {
+					return viewWriter.createAttributeElement( 'b' );
 				}
 			};
 
@@ -738,7 +739,9 @@ describe( 'downcast-converters', () => {
 				new ModelText( 'x' )
 			] );
 
-			const elementGenerator = ( href, data, consumable, api ) => api.writer.createAttributeElement( 'a', { href } );
+			const elementGenerator = ( modelAttributeValue, viewWriter ) => {
+				return viewWriter.createAttributeElement( 'a', { href: modelAttributeValue } );
+			};
 
 			dispatcher.on( 'attribute:link', wrap( elementGenerator ) );
 
@@ -758,7 +761,7 @@ describe( 'downcast-converters', () => {
 
 		it( 'should support unicode', () => {
 			const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
-			const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' );
+			const creator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' );
 
 			dispatcher.on( 'attribute:bold', wrap( creator ) );
 
@@ -777,18 +780,20 @@ describe( 'downcast-converters', () => {
 
 		it( 'should be possible to override wrap', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
-			const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' );
 
-			dispatcher.on( 'attribute:bold', wrap( creator ) );
-			dispatcher.on( 'attribute:bold', ( evt, data, consumable ) => {
-				consumable.consume( data.item, 'attribute:bold' );
-			}, { priority: 'high' } );
+			dispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ) ) );
+
+			dispatcher.on(
+				'attribute:bold',
+				wrap( ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ) ),
+				{ priority: 'high' }
+			);
 
 			model.change( writer => {
 				writer.insert( modelElement, modelRootStart );
 			} );
 
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p><strong>foobar</strong></p></div>' );
 		} );
 
 		it( 'should not convert and not consume if creator function returned null', () => {
@@ -799,9 +804,9 @@ describe( 'downcast-converters', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) );
 
 			dispatcher.on( 'attribute:italic', wrap( elementGenerator ) );
-			dispatcher.on( 'attribute:italic', ( evt, data, consumable ) => {
-				expect( consumable.test( data.item, 'attribute:italic' ) ).to.be.true;
-			} );
+
+			const spy = sinon.spy();
+			dispatcher.on( 'attribute:italic', spy );
 
 			model.change( writer => {
 				writer.insert( modelElement, modelRootStart );
@@ -809,6 +814,7 @@ describe( 'downcast-converters', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 			expect( dispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true;
+			expect( spy.called ).to.be.true;
 		} );
 	} );
 
@@ -829,30 +835,11 @@ describe( 'downcast-converters', () => {
 				range = ModelRange.createFromParentsAndOffsets( modelElement, 3, modelElement, 3 );
 			} );
 
-			it( 'should insert and remove ui element - element as a creator', () => {
-				const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
-
-				dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
-				dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
+			it( 'should insert and remove ui element', () => {
+				const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } );
 
-				model.change( writer => {
-					writer.setMarker( 'marker', range );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo<span class="marker"></span>bar</p></div>' );
-
-				model.change( writer => {
-					writer.removeMarker( 'marker' );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-			} );
-
-			it( 'should insert and remove ui element - function as a creator', () => {
-				const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
-
-				dispatcher.on( 'addMarker:marker', insertUIElement( () => viewUi ) );
-				dispatcher.on( 'removeMarker:marker', removeUIElement( () => viewUi ) );
+				dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
+				dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) );
 
 				model.change( writer => {
 					writer.setMarker( 'marker', range );
@@ -868,16 +855,19 @@ describe( 'downcast-converters', () => {
 			} );
 
 			it( 'should not convert if consumable was consumed', () => {
-				const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
-
 				sinon.spy( dispatcher, 'fire' );
 
-				dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
+				dispatcher.on( 'addMarker:marker', insertUIElement(
+					( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ) )
+				);
+
 				dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
 					consumable.consume( data.markerRange, 'addMarker:marker' );
 				}, { priority: 'high' } );
 
-				dispatcher.convertMarkerAdd( 'marker', range );
+				model.change( writer => {
+					writer.setMarker( 'marker', range );
+				} );
 
 				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 				expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
@@ -907,10 +897,10 @@ describe( 'downcast-converters', () => {
 			} );
 
 			it( 'should insert and remove ui element - element as a creator', () => {
-				const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
+				const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } );
 
-				dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
-				dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
+				dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
+				dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) );
 
 				model.change( writer => {
 					writer.setMarker( 'marker', range );
@@ -927,10 +917,10 @@ describe( 'downcast-converters', () => {
 			} );
 
 			it( 'should insert and remove ui element - function as a creator', () => {
-				const viewUi = data => new ViewUIElement( 'span', { 'class': data.markerName } );
+				const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': data.markerName } );
 
-				dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
-				dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
+				dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
+				dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) );
 
 				model.change( writer => {
 					writer.setMarker( 'marker', range );
@@ -947,12 +937,12 @@ describe( 'downcast-converters', () => {
 			} );
 
 			it( 'should insert and remove different opening and ending element', () => {
-				function creator( data ) {
+				function creator( data, viewWriter ) {
 					if ( data.isOpening ) {
-						return new ViewUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
+						return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
 					}
 
-					return new ViewUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
+					return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
 				}
 
 				dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
@@ -974,16 +964,18 @@ describe( 'downcast-converters', () => {
 			} );
 
 			it( 'should not convert if consumable was consumed', () => {
-				const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
+				const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } );
 
 				sinon.spy( dispatcher, 'fire' );
 
-				dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
+				dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
 				dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
 					consumable.consume( data.item, 'addMarker:marker' );
 				}, { priority: 'high' } );
 
-				dispatcher.convertMarkerAdd( 'marker', range );
+				model.change( writer => {
+					writer.setMarker( 'marker', range );
+				} );
 
 				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 				expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
@@ -1099,7 +1091,9 @@ describe( 'downcast-converters', () => {
 			const modelP = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
 			const modelWidget = new ModelElement( 'widget', null, modelP );
 
-			dispatcher.on( 'insert:widget', insertElement( () => new ViewContainerElement( 'widget' ) ) );
+			dispatcher.on( 'insert:widget', insertElement(
+				( modelElement, viewWriter ) => viewWriter.createContainerElement( 'widget' ) )
+			);
 
 			model.change( writer => {
 				writer.insert( modelWidget, modelRootStart );

+ 2 - 2
packages/ckeditor5-engine/tests/conversion/downcast-selection-converters.js

@@ -58,7 +58,7 @@ describe( 'downcast-selection-converters', () => {
 
 		dispatcher.on( 'insert:$text', insertText() );
 
-		const strongCreator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'strong' );
+		const strongCreator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' );
 		dispatcher.on( 'attribute:bold', wrap( strongCreator ) );
 
 		dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
@@ -502,7 +502,7 @@ describe( 'downcast-selection-converters', () => {
 			model.schema.extend( '$text', { allowIn: 'td' } );
 
 			// "Universal" converter to convert table structure.
-			const containerCreator = ( item, consumable, api ) => api.writer.createContainerElement( item.name );
+			const containerCreator = ( modelElement, viewWriter ) => viewWriter.createContainerElement( modelElement.name );
 			const tableConverter = insertElement( containerCreator );
 			dispatcher.on( 'insert:table', tableConverter );
 			dispatcher.on( 'insert:tr', tableConverter );

+ 11 - 12
packages/ckeditor5-engine/tests/conversion/upcast-converters.js

@@ -74,7 +74,7 @@ describe( 'upcast-helpers', () => {
 			} );
 
 			const helperA = upcastElementToElement( { view: 'p', model: 'p' } );
-			const helperB = upcastElementToElement( { view: 'p', model: 'paragraph' }, 'high' );
+			const helperB = upcastElementToElement( { view: 'p', model: 'paragraph', priority: 'high' } );
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 
@@ -86,18 +86,16 @@ describe( 'upcast-helpers', () => {
 				inheritAllFrom: '$block'
 			} );
 
-			const helperParagraph = upcastElementToElement( { view: 'p', model: 'paragraph' } );
 			const helperFancy = upcastElementToElement( {
 				view: {
 					name: 'p',
 					class: 'fancy'
 				},
-				model: 'fancyParagraph'
-			}, 'high' );
+				model: 'fancyParagraph',
+			} );
 
-			conversion.for( 'upcast' ).add( helperParagraph ).add( helperFancy );
+			conversion.for( 'upcast' ).add( helperFancy );
 
-			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
 			expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<fancyParagraph></fancyParagraph>' );
 		} );
 
@@ -160,7 +158,7 @@ describe( 'upcast-helpers', () => {
 
 		it( 'should not do anything if returned model element is null', () => {
 			const helperA = upcastElementToElement( { view: 'p', model: 'paragraph' } );
-			const helperB = upcastElementToElement( { view: 'p', model: () => null }, 'high' );
+			const helperB = upcastElementToElement( { view: 'p', model: () => null, priority: 'high' } );
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 
@@ -182,7 +180,7 @@ describe( 'upcast-helpers', () => {
 
 		it( 'can be overwritten using priority', () => {
 			const helperA = upcastElementToAttribute( { view: 'strong', model: 'strong' } );
-			const helperB = upcastElementToAttribute( { view: 'strong', model: 'bold' }, 'high' );
+			const helperB = upcastElementToAttribute( { view: 'strong', model: 'bold', priority: 'high' } );
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 
@@ -298,8 +296,9 @@ describe( 'upcast-helpers', () => {
 				model: {
 					key: 'bold',
 					value: () => null
-				}
-			}, 'high' );
+				},
+				priority: 'high'
+			} );
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 
@@ -355,7 +354,7 @@ describe( 'upcast-helpers', () => {
 			} );
 
 			const helperA = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'src' } );
-			const helperB = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
+			const helperB = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', priority: 'normal' } );
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 
@@ -520,7 +519,7 @@ describe( 'upcast-helpers', () => {
 
 		it( 'can be overwritten using priority', () => {
 			const helperA = upcastElementToMarker( { view: 'marker-search', model: 'search-result' } );
-			const helperB = upcastElementToMarker( { view: 'marker-search', model: 'search' }, 'high' );
+			const helperB = upcastElementToMarker( { view: 'marker-search', model: 'search', priority: 'high' } );
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );