浏览代码

Changed: Moved `conversion.two-way-converters` to `conversion.Conversion`.

Szymon Cofalik 7 年之前
父节点
当前提交
9717b27604

+ 389 - 0
packages/ckeditor5-engine/src/conversion/conversion.js

@@ -9,6 +9,18 @@
 
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
+import {
+	downcastElementToElement,
+	downcastAttributeToElement,
+	downcastAttributeToAttribute
+} from './downcast-converters';
+
+import {
+	upcastElementToElement,
+	upcastElementToAttribute,
+	upcastAttributeToAttribute
+} from './upcast-converters';
+
 /**
  * An utility class that helps organizing dispatchers and adding converters to them.
  */
@@ -127,6 +139,352 @@ export default class Conversion {
 
 		return dispatchers;
 	}
+
+	/**
+	 * Defines a conversion between the model and the view where a model element is represented as a view element (and vice versa).
+	 * For example, model `<paragraph>Foo</paragraph>` is `<p>Foo</p>` in the view.
+	 *
+	 *		conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+	 *
+	 *		conversion.elementToElement( {
+	 *			model: 'fancyParagraph',
+	 *			view: {
+	 *				name: 'p',
+	 *				class: 'fancy'
+	 *			}
+	 *		} );
+	 *
+	 *		conversion.elementToElement( {
+	 *			model: 'paragraph',
+	 *			view: 'p',
+	 *			upcastAlso: [
+	 *				'div',
+	 *				{
+	 *					// Match any name.
+	 *					name: /./,
+	 *					style: {
+	 *						display: 'block'
+	 *					}
+	 *				}
+	 *			]
+	 *		} );
+	 *
+	 *		conversion.elementToElement( {
+	 *			model: 'heading',
+	 *			view: 'h2',
+	 *			// Convert "headling-like" paragraphs to headings.
+	 *			upcastAlso: viewElement => {
+	 *				const fontSize = viewElement.getStyle( 'font-size' );
+	 *
+	 *				if ( !fontSize ) {
+	 *					return null;
+	 *				}
+	 *
+	 *				const match = fontSize.match( /(\d+)\s*px/ );
+	 *
+	 *				if ( !match ) {
+	 *					return null;
+	 *				}
+	 *
+	 *				const size = Number( match[ 1 ] );
+	 *
+	 *				if ( size > 26 ) {
+	 *					return { name: true, style: [ 'font-size' ] };
+	 *				}
+	 *
+	 *				return null;
+	 *			}
+	 *		} );
+	 *
+	 * @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.
+	 */
+	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 ) );
+		}
+	}
+
+	/**
+	 * Defines a conversion between the model and the view where a model attribute is represented as 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' } );
+	 *
+	 *		conversion.attributeToElement( 'bold', {
+	 *			view: {
+	 *				name: 'span',
+	 *				class: 'bold'
+	 *			}
+	 *		} );
+	 *
+	 *		conversion.attributeToElement( 'bold', {
+	 *			view: 'strong',
+	 *			upcastAlso: [
+	 *				'b',
+	 *				{
+	 *					name: 'span',
+	 *					class: 'bold'
+	 *				},
+	 *				{
+	 *					name: 'span',
+	 *					style: {
+	 *						'font-weight': 'bold'
+	 *					}
+	 *				},
+	 *				viewElement => {
+	 *					const fontWeight = viewElement.getStyle( 'font-weight' );
+	 *
+	 *					if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
+	 *						return {
+	 *							name: true,
+	 *							style: [ 'font-weight' ]
+	 *						};
+	 *					}
+	 *				}
+	 *			]
+	 *		} );
+	 *
+	 *		conversion.attributeToElement( 'styled', {
+	 *			model: 'dark',
+	 *			view: {
+	 *				name: 'span',
+	 *				class: [ 'styled', 'styled-dark' ]
+	 *			}
+	 *		} );
+	 *
+	 *		conversion.attributeToElement( 'fontSize', [
+	 *			{
+	 *				model: 'big',
+	 *				view: {
+	 *					name: 'span',
+	 *					style: {
+	 *						'font-size': '1.2em'
+	 *					}
+	 *				}
+	 *			},
+	 *			{
+	 *				model: 'small',
+	 *				view: {
+	 *					name: 'span',
+	 *					style: {
+	 *						'font-size': '0.8em'
+	 *					}
+	 *				}
+	 *			}
+	 *		] );
+	 *
+	 *		conversion.attributeToElement( 'fontSize', [
+	 *			{
+	 *				model: 'big',
+	 *				view: {
+	 *					name: 'span',
+	 *					style: {
+	 *						'font-size': '1.2em'
+	 *					}
+	 *				},
+	 *				upcastAlso: viewElement => {
+	 *					const fontSize = viewElement.getStyle( 'font-size' );
+	 *
+	 *					if ( !fontSize ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const match = fontSize.match( /(\d+)\s*px/ );
+	 *
+	 *					if ( !match ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const size = Number( match[ 1 ] );
+	 *
+	 *					if ( viewElement.is( 'span' ) && size > 10 ) {
+	 *						return { name: true, style: [ 'font-size' ] };
+	 *					}
+	 *
+	 *					return null;
+	 *				}
+	 *			},
+	 *			{
+	 *				model: 'small',
+	 *				view: {
+	 *					name: 'span',
+	 *					style: {
+	 *						'font-size': '0.8em'
+	 *					}
+	 *				},
+	 *				upcastAlso: viewElement => {
+	 *					const fontSize = viewElement.getStyle( 'font-size' );
+	 *
+	 *					if ( !fontSize ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const match = fontSize.match( /(\d+)\s*px/ );
+	 *
+	 *					if ( !match ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const size = Number( match[ 1 ] );
+	 *
+	 *					if ( viewElement.is( 'span' ) && size < 10 ) {
+	 *						return { name: true, style: [ 'font-size' ] };
+	 *					}
+	 *
+	 *					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.
+	 */
+	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';
+
+				this.for( 'upcast' ).add( upcastElementToAttribute( {
+					view,
+					model
+				}, 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,
+	 *			view: {
+	 *				name: 'img',
+	 *				key: 'class',
+	 *				value: 'aside half-size'
+	 *			}
+	 *		} );
+	 *
+	 *		conversion.attributeToAttribute( 'styled', [
+	 *			{
+	 *				model: 'dark',
+	 *				view: {
+	 *					key: 'class',
+	 *					value: 'styled styled-dark'
+	 *				}
+	 *			},
+	 *			{
+	 *				model: 'light',
+	 *				view: {
+	 *					key: 'class',
+	 *					value: 'styled styled-light'
+	 *				}
+	 *			}
+	 *		] );
+	 *
+	 *		conversion.attributeToAttribute( 'align', [
+	 *			{
+	 *				model: 'right',
+	 *				view: {
+	 *					key: 'class',
+	 *					value: 'align-right'
+	 *				},
+	 *				upcastAlso: viewElement => {
+	 *					if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
+	 *						return {
+	 *							style: [ 'text-align' ]
+	 *						};
+	 *					}
+	 *
+	 *					return null;
+	 *				}
+	 *			},
+	 *			{
+	 *				model: 'center',
+	 *				view: {
+	 *					key: 'class',
+	 *					value: 'align-center'
+	 *				},
+	 *				upcastAlso: {
+	 *					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 {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.
+	 */
+	attributeToAttribute( modelAttributeKey, 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 };
+		}
+
+		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 ? 'low' : 'normal';
+
+				this.for( 'upcast' ).add( upcastAttributeToAttribute( {
+					view,
+					model
+				}, priority ) );
+			}
+		}
+	}
 }
 
 // Helper function for `Conversion` `.add()` method.
@@ -143,3 +501,34 @@ function _addToDispatchers( dispatchers, conversionHelper ) {
 		conversionHelper( dispatcher );
 	}
 }
+
+// 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.
+// @returns {Array} Array containing view definitions.
+function _getAllViews( definition ) {
+	if ( !definition.upcastAlso ) {
+		return [ definition.view ];
+	} else {
+		const upcastAlso = Array.isArray( definition.upcastAlso ) ? definition.upcastAlso : [ definition.upcastAlso ];
+
+		return [ definition.view ].concat( upcastAlso );
+	}
+}

+ 0 - 400
packages/ckeditor5-engine/src/conversion/two-way-converters.js

@@ -1,400 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/conversion/two-way-converters
- */
-
-import {
-	downcastElementToElement,
-	downcastAttributeToElement,
-	downcastAttributeToAttribute
-} from './downcast-converters';
-
-import {
-	upcastElementToElement,
-	upcastElementToAttribute,
-	upcastAttributeToAttribute
-} from './upcast-converters';
-
-/**
- * Defines a conversion between the model and the view where a model element is represented as a view element (and vice versa).
- * For example, model `<paragraph>Foo</paragraph>` is `<p>Foo</p>` in the view.
- *
- *		elementToElement( conversion, { model: 'paragraph', view: 'p' } );
- *
- *		elementToElement( conversion, {
- *			model: 'fancyParagraph',
- *			view: {
- *				name: 'p',
- *				class: 'fancy'
- *			}
- *		} );
- *
- *		elementToElement( conversion, {
- *			model: 'paragraph',
- *			view: 'p',
- *			upcastAlso: [
- *				'div',
- *				{
- *					// Match any name.
- *					name: /./,
- *					style: {
- *						display: 'block'
- *					}
- *				}
- *			]
- *		} );
- *
- *		elementToElement( conversion, {
- *			model: 'heading',
- *			view: 'h2',
- *			// Convert "headling-like" paragraphs to headings.
- *			upcastAlso: viewElement => {
- *				const fontSize = viewElement.getStyle( 'font-size' );
- *
- *				if ( !fontSize ) {
- *					return null;
- *				}
- *
- *				const match = fontSize.match( /(\d+)\s*px/ );
- *
- *				if ( !match ) {
- *					return null;
- *				}
- *
- *				const size = Number( match[ 1 ] );
- *
- *				if ( size > 26 ) {
- *					return { name: true, style: [ 'font-size' ] };
- *				}
- *
- *				return null;
- *			}
- *		} );
- *
- * @param {module:engine/conversion/conversion~Conversion} conversion Conversion class instance with registered conversion dispatchers.
- * @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.
- */
-export function elementToElement( conversion, definition ) {
-	// Set up downcast converter.
-	conversion.for( 'downcast' ).add( downcastElementToElement( definition ) );
-
-	// Set up upcast converter.
-	for ( const view of _getAllViews( definition ) ) {
-		const priority = view == definition.view ? 'normal' : 'high';
-
-		conversion.for( 'upcast' ).add( upcastElementToElement( {
-			model: definition.model,
-			view
-		}, priority ) );
-	}
-}
-
-/**
- * Defines a conversion between the model and the view where a model attribute is represented as a view element (and vice versa).
- * For example, model text node with data `"Foo"` and `bold` attribute is `<strong>Foo</strong>` in the view.
- *
- *		attributeToElement( conversion, 'bold', { view: 'strong' } );
- *
- *		attributeToElement( conversion, 'bold', {
- *			view: {
- *				name: 'span',
- *				class: 'bold'
- *			}
- *		} );
- *
- *		attributeToElement( conversion, 'bold', {
- *			view: 'strong',
- *			upcastAlso: [
- *				'b',
- *				{
- *					name: 'span',
- *					class: 'bold'
- *				},
- *				{
- *					name: 'span',
- *					style: {
- *						'font-weight': 'bold'
- *					}
- *				},
- *				viewElement => {
- *					const fontWeight = viewElement.getStyle( 'font-weight' );
- *
- *					if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
- *						return {
- *							name: true,
- *							style: [ 'font-weight' ]
- *						};
- *					}
- *				}
- *			]
- *		} );
- *
- *		attributeToElement( conversion, 'styled', {
- *			model: 'dark',
- *			view: {
- *				name: 'span',
- *				class: [ 'styled', 'styled-dark' ]
- *			}
- *		} );
- *
- *		attributeToElement( conversion, 'fontSize', [
- *			{
- *				model: 'big',
- *				view: {
- *					name: 'span',
- *					style: {
- *						'font-size': '1.2em'
- *					}
- *				}
- *			},
- *			{
- *				model: 'small',
- *				view: {
- *					name: 'span',
- *					style: {
- *						'font-size': '0.8em'
- *					}
- *				}
- *			}
- *		] );
- *
- *		attributeToElement( conversion, 'fontSize', [
- *			{
- *				model: 'big',
- *				view: {
- *					name: 'span',
- *					style: {
- *						'font-size': '1.2em'
- *					}
- *				},
- *				upcastAlso: viewElement => {
- *					const fontSize = viewElement.getStyle( 'font-size' );
- *
- *					if ( !fontSize ) {
- *						return null;
- *					}
- *
- *					const match = fontSize.match( /(\d+)\s*px/ );
- *
- *					if ( !match ) {
- *						return null;
- *					}
- *
- *					const size = Number( match[ 1 ] );
- *
- *					if ( viewElement.is( 'span' ) && size > 10 ) {
- *						return { name: true, style: [ 'font-size' ] };
- *					}
- *
- *					return null;
- *				}
- *			},
- *			{
- *				model: 'small',
- *				view: {
- *					name: 'span',
- *					style: {
- *						'font-size': '0.8em'
- *					}
- *				},
- *				upcastAlso: viewElement => {
- *					const fontSize = viewElement.getStyle( 'font-size' );
- *
- *					if ( !fontSize ) {
- *						return null;
- *					}
- *
- *					const match = fontSize.match( /(\d+)\s*px/ );
- *
- *					if ( !match ) {
- *						return null;
- *					}
- *
- *					const size = Number( match[ 1 ] );
- *
- *					if ( viewElement.is( 'span' ) && size < 10 ) {
- *						return { name: true, style: [ 'font-size' ] };
- *					}
- *
- *					return null;
- *				}
- *			}
- *		] );
- *
- * @param {module:engine/conversion/conversion~Conversion} conversion Conversion class instance with registered conversion dispatchers.
- * @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.
- */
-export function attributeToElement( conversion, modelAttributeKey, definition ) {
-	// Set downcast (model to view conversion).
-	conversion.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';
-
-			conversion.for( 'upcast' ).add( upcastElementToAttribute( {
-				view,
-				model
-			}, 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).
- *
- *		attributeToAttribute( conversion, 'src' );
- *
- *		attributeToAttribute( conversion, 'source', { view: 'src' } );
- *
- *		attributeToAttribute( conversion, 'aside', {
- *			model: true,
- *			view: {
- *				name: 'img',
- *				key: 'class',
- *				value: 'aside half-size'
- *			}
- *		} );
- *
- *		attributeToAttribute( conversion, 'styled', [
- *			{
- *				model: 'dark',
- *				view: {
- *					key: 'class',
- *					value: 'styled styled-dark'
- *				}
- *			},
- *			{
- *				model: 'light',
- *				view: {
- *					key: 'class',
- *					value: 'styled styled-light'
- *				}
- *			}
- *		] );
- *
- *		attributeToAttribute( conversion, 'align', [
- *			{
- *				model: 'right',
- *				view: {
- *					key: 'class',
- *					value: 'align-right'
- *				},
- *				upcastAlso: viewElement => {
- *					if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
- *						return {
- *							style: [ 'text-align' ]
- *						};
- *					}
- *
- *					return null;
- *				}
- *			},
- *			{
- *				model: 'center',
- *				view: {
- *					key: 'class',
- *					value: 'align-center'
- *				},
- *				upcastAlso: {
- *					style: {
- *						'text-align': 'center'
- *					}
- *				}
- *			}
- *		] );
- *
- * @param {module:engine/conversion/conversion~Conversion} conversion Conversion class instance with registered conversion dispatchers.
- * @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 {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.
- */
-export function attributeToAttribute( conversion, modelAttributeKey, definition ) {
-	// Set up downcast converter.
-	conversion.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 };
-	}
-
-	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 ? 'low' : 'normal';
-
-			conversion.for( 'upcast' ).add( upcastAttributeToAttribute( {
-				view,
-				model
-			}, priority ) );
-		}
-	}
-}
-
-// 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.
-// @returns {Array} Array containing view definitions.
-function _getAllViews( definition ) {
-	if ( !definition.upcastAlso ) {
-		return [ definition.view ];
-	} else {
-		const upcastAlso = Array.isArray( definition.upcastAlso ) ? definition.upcastAlso : [ definition.upcastAlso ];
-
-		return [ definition.view ].concat( upcastAlso );
-	}
-}

+ 531 - 0
packages/ckeditor5-engine/tests/conversion/conversion.js

@@ -6,6 +6,18 @@
 import Conversion from '../../src/conversion/conversion';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
+import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
+
+import { convertText, convertToModelFragment } from '../../src/conversion/upcast-converters';
+
+import EditingController from '../../src/controller/editingcontroller';
+
+import Model from '../../src/model/model';
+import ModelRange from '../../src/model/range';
+
+import { stringify as viewStringify, parse as viewParse } from '../../src/dev-utils/view';
+import { stringify as modelStringify } from '../../src/dev-utils/model';
+
 describe( 'Conversion', () => {
 	let conversion, dispA, dispB;
 
@@ -70,4 +82,523 @@ describe( 'Conversion', () => {
 			expect( helperB.calledWithExactly( dispB ) ).to.be.true;
 		} );
 	} );
+
+	describe( 'converters', () => {
+		let viewDispatcher, model, schema, conversion, modelRoot, viewRoot;
+
+		beforeEach( () => {
+			model = new Model();
+			const controller = new EditingController( model );
+
+			const modelDoc = model.document;
+			modelRoot = modelDoc.createRoot();
+
+			viewRoot = controller.view.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';
+
+			schema = model.schema;
+
+			schema.extend( '$text', {
+				allowAttributes: [ 'bold' ]
+			} );
+
+			schema.register( 'paragraph', {
+				inheritAllFrom: '$block'
+			} );
+
+			viewDispatcher = new UpcastDispatcher( model, { schema } );
+			viewDispatcher.on( 'text', convertText() );
+			viewDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+			viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
+
+			conversion = new Conversion();
+			conversion.register( 'upcast', [ viewDispatcher ] );
+			conversion.register( 'downcast', [ controller.downcastDispatcher ] );
+		} );
+
+		describe( 'elementToElement', () => {
+			it( 'config.view is a string', () => {
+				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+
+				test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
+			} );
+
+			it( 'config.view is an object', () => {
+				schema.register( 'fancyParagraph', {
+					inheritAllFrom: 'paragraph'
+				} );
+
+				conversion.elementToElement( {
+					model: 'fancyParagraph',
+					view: {
+						name: 'p',
+						class: 'fancy'
+					}
+				} );
+
+				test( '<p class="fancy">Foo</p>', '<fancyParagraph>Foo</fancyParagraph>' );
+			} );
+
+			it( 'config.view is an object with upcastAlso defined', () => {
+				conversion.elementToElement( {
+					model: 'paragraph',
+					view: 'p',
+					upcastAlso: [
+						'div',
+						{
+							// Match any name.
+							name: /./,
+							style: {
+								display: 'block'
+							}
+						}
+					]
+				} );
+
+				test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
+				test( '<div>Foo</div>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
+				test( '<span style="display:block">Foo</span>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
+			} );
+
+			it( 'upcastAlso given as a function', () => {
+				schema.register( 'heading', {
+					inheritAllFrom: '$block'
+				} );
+
+				conversion.elementToElement( {
+					model: 'heading',
+					view: 'h2',
+					upcastAlso: viewElement => {
+						const fontSize = viewElement.getStyle( 'font-size' );
+
+						if ( !fontSize ) {
+							return null;
+						}
+
+						const match = fontSize.match( /(\d+)\s*px/ );
+
+						if ( !match ) {
+							return null;
+						}
+
+						const size = Number( match[ 1 ] );
+
+						if ( size >= 26 ) {
+							return { name: true, style: [ 'font-size' ] };
+						}
+
+						return null;
+					}
+				} );
+
+				conversion.elementToElement( {
+					model: 'paragraph',
+					view: 'p'
+				} );
+
+				test( '<p></p>', '<paragraph></paragraph>' );
+				test( '<p style="font-size:20px"></p>', '<paragraph></paragraph>', '<p></p>' );
+
+				test( '<h2></h2>', '<heading></heading>' );
+				test( '<p style="font-size:26px"></p>', '<heading></heading>', '<h2></h2>' );
+			} );
+		} );
+
+		describe( 'attributeToElement', () => {
+			beforeEach( () => {
+				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+			} );
+
+			it( 'config.view is a string', () => {
+				conversion.attributeToElement( 'bold', { view: 'strong' } );
+
+				test( '<p><strong>Foo</strong> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
+			} );
+
+			it( 'config.view is an object', () => {
+				conversion.attributeToElement( 'bold', {
+					view: {
+						name: 'span',
+						class: 'bold'
+					}
+				} );
+
+				test( '<p><span class="bold">Foo</span> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
+			} );
+
+			it( 'config.view is an object with upcastAlso defined', () => {
+				conversion.attributeToElement( 'bold', {
+					view: 'strong',
+					upcastAlso: [
+						'b',
+						{
+							name: 'span',
+							class: 'bold'
+						},
+						{
+							name: 'span',
+							style: {
+								'font-weight': 'bold'
+							}
+						},
+						viewElement => {
+							const fontWeight = viewElement.getStyle( 'font-weight' );
+
+							if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test( fontWeight ) && Number( fontWeight ) > 500 ) {
+								return {
+									name: true,
+									style: [ 'font-weight' ]
+								};
+							}
+						}
+					]
+				} );
+
+				test(
+					'<p><strong>Foo</strong></p>',
+					'<paragraph><$text bold="true">Foo</$text></paragraph>'
+				);
+
+				test(
+					'<p><b>Foo</b></p>',
+					'<paragraph><$text bold="true">Foo</$text></paragraph>',
+					'<p><strong>Foo</strong></p>'
+				);
+
+				test(
+					'<p><span class="bold">Foo</span></p>',
+					'<paragraph><$text bold="true">Foo</$text></paragraph>',
+					'<p><strong>Foo</strong></p>'
+				);
+
+				test(
+					'<p><span style="font-weight: bold;">Foo</span></p>',
+					'<paragraph><$text bold="true">Foo</$text></paragraph>',
+					'<p><strong>Foo</strong></p>'
+				);
+
+				test(
+					'<p><span style="font-weight: 500;">Foo</span></p>',
+					'<paragraph>Foo</paragraph>',
+					'<p>Foo</p>'
+				);
+
+				test(
+					'<p><span style="font-weight: 600;">Foo</span></p>',
+					'<paragraph><$text bold="true">Foo</$text></paragraph>',
+					'<p><strong>Foo</strong></p>'
+				);
+			} );
+
+			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', () => {
+				schema.extend( '$text', {
+					allowAttributes: [ 'fontSize' ]
+				} );
+
+				conversion.attributeToElement( 'fontSize', [
+					{
+						model: 'big',
+						view: {
+							name: 'span',
+							style: {
+								'font-size': '1.2em'
+							}
+						}
+					},
+					{
+						model: 'small',
+						view: {
+							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 => {
+							const fontSize = viewElement.getStyle( 'font-size' );
+
+							if ( !fontSize ) {
+								return null;
+							}
+
+							const match = fontSize.match( /(\d+)\s*px/ );
+
+							if ( !match ) {
+								return null;
+							}
+
+							const size = Number( match[ 1 ] );
+
+							if ( viewElement.is( 'span' ) && size > 10 ) {
+								return { name: true, style: [ 'font-size' ] };
+							}
+
+							return null;
+						}
+					},
+					{
+						model: 'small',
+						view: {
+							name: 'span',
+							style: {
+								'font-size': '0.8em'
+							}
+						},
+						upcastAlso: viewElement => {
+							const fontSize = viewElement.getStyle( 'font-size' );
+
+							if ( !fontSize ) {
+								return null;
+							}
+
+							const match = fontSize.match( /(\d+)\s*px/ );
+
+							if ( !match ) {
+								return null;
+							}
+
+							const size = Number( match[ 1 ] );
+
+							if ( viewElement.is( 'span' ) && size < 10 ) {
+								return { name: true, style: [ 'font-size' ] };
+							}
+
+							return null;
+						}
+					}
+				] );
+
+				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:12px">Foo</span> bar</p>',
+					'<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>',
+					'<p><span style="font-size:1.2em">Foo</span> bar</p>'
+				);
+
+				test(
+					'<p><span style="font-size:0.8em">Foo</span> bar</p>',
+					'<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>'
+				);
+
+				test(
+					'<p><span style="font-size:8px">Foo</span> bar</p>',
+					'<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>',
+					'<p><span style="font-size:0.8em">Foo</span> bar</p>'
+				);
+
+				test(
+					'<p><span style="font-size:10px">Foo</span> bar</p>',
+					'<paragraph>Foo bar</paragraph>',
+					'<p>Foo bar</p>'
+				);
+			} );
+		} );
+
+		describe( 'attributeToAttribute', () => {
+			beforeEach( () => {
+				conversion.elementToElement( { model: 'image', view: 'img' } );
+
+				schema.register( 'image', {
+					inheritAllFrom: '$block',
+				} );
+			} );
+
+			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', () => {
+				schema.extend( 'image', {
+					allowAttributes: [ 'source' ]
+				} );
+
+				conversion.attributeToAttribute( 'source', { view: 'src' } );
+
+				test( '<img src="foo.jpg"></img>', '<image source="foo.jpg"></image>' );
+			} );
+
+			it( 'config.view is an object', () => {
+				schema.extend( 'image', {
+					allowAttributes: [ 'aside' ]
+				} );
+
+				conversion.attributeToAttribute( 'aside', {
+					model: true,
+					view: {
+						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( '<p class="aside half-size"></p>', '<paragraph></paragraph>', '<p></p>' );
+			} );
+
+			it( 'config is an array', () => {
+				schema.extend( 'image', {
+					allowAttributes: [ 'styled' ]
+				} );
+
+				conversion.attributeToAttribute( 'styled', [
+					{
+						model: 'dark',
+						view: {
+							key: 'class',
+							value: 'styled styled-dark'
+						}
+					},
+					{
+						model: 'light',
+						view: {
+							key: 'class',
+							value: 'styled styled-light'
+						}
+					}
+				] );
+
+				test( '<img class="styled styled-dark"></img>', '<image styled="dark"></image>' );
+				test( '<img class="styled styled-light"></img>', '<image styled="light"></image>' );
+			} );
+
+			it( 'config is an array with upcastAlso defined', () => {
+				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+
+				schema.extend( 'paragraph', {
+					allowAttributes: [ 'align' ]
+				} );
+
+				conversion.attributeToAttribute( 'align', [
+					{
+						model: 'right',
+						view: {
+							key: 'class',
+							value: 'align-right'
+						},
+						upcastAlso: viewElement => {
+							if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
+								return {
+									style: [ 'text-align' ]
+								};
+							}
+
+							return null;
+						}
+					},
+					{
+						model: 'center',
+						view: {
+							key: 'class',
+							value: 'align-center'
+						},
+						upcastAlso: {
+							style: {
+								'text-align': 'center'
+							}
+						}
+					}
+				] );
+
+				test(
+					'<p class="align-right">Foo</p>',
+					'<paragraph align="right">Foo</paragraph>'
+				);
+
+				test(
+					'<p style="text-align:right">Foo</p>',
+					'<paragraph align="right">Foo</paragraph>',
+					'<p class="align-right">Foo</p>'
+				);
+
+				test(
+					'<p class="align-center">Foo</p>',
+					'<paragraph align="center">Foo</paragraph>'
+				);
+
+				test(
+					'<p style="text-align:center">Foo</p>',
+					'<paragraph align="center">Foo</paragraph>',
+					'<p class="align-center">Foo</p>'
+				);
+			} );
+		} );
+
+		function test( input, expectedModel, expectedView = null ) {
+			loadData( input );
+
+			expect( modelStringify( model.document.getRoot() ) ).to.equal( expectedModel );
+			expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( expectedView || input );
+		}
+
+		function loadData( input ) {
+			const parsedView = viewParse( input );
+
+			const convertedModel = viewDispatcher.convert( parsedView );
+
+			model.change( writer => {
+				writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, modelRoot.maxOffset ) );
+				writer.insert( convertedModel, modelRoot, 0 );
+			} );
+		}
+	} );
 } );

+ 0 - 536
packages/ckeditor5-engine/tests/conversion/two-way-converters.js

@@ -1,536 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import {
-	elementToElement, attributeToElement, attributeToAttribute
-} from '../../src/conversion/two-way-converters';
-
-import Conversion from '../../src/conversion/conversion';
-import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
-
-import { convertText, convertToModelFragment } from '../../src/conversion/upcast-converters';
-
-import EditingController from '../../src/controller/editingcontroller';
-
-import Model from '../../src/model/model';
-import ModelRange from '../../src/model/range';
-
-import { stringify as viewStringify, parse as viewParse } from '../../src/dev-utils/view';
-import { stringify as modelStringify } from '../../src/dev-utils/model';
-
-describe( 'two-way-converters', () => {
-	let upcastDispatcher, model, schema, conversion, modelRoot, viewRoot;
-
-	beforeEach( () => {
-		model = new Model();
-		const controller = new EditingController( model );
-
-		const modelDoc = model.document;
-		modelRoot = modelDoc.createRoot();
-
-		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';
-
-		schema = model.schema;
-
-		schema.extend( '$text', {
-			allowAttributes: [ 'bold' ]
-		} );
-
-		schema.register( 'paragraph', {
-			inheritAllFrom: '$block'
-		} );
-
-		upcastDispatcher = new UpcastDispatcher( { schema } );
-		upcastDispatcher.on( 'text', convertText() );
-		upcastDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
-		upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
-
-		conversion = new Conversion();
-		conversion.register( 'upcast', [ upcastDispatcher ] );
-		conversion.register( 'downcast', [ controller.downcastDispatcher ] );
-	} );
-
-	describe( 'elementToElement', () => {
-		it( 'config.view is a string', () => {
-			elementToElement( conversion, { model: 'paragraph', view: 'p' } );
-
-			test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
-		} );
-
-		it( 'config.view is an object', () => {
-			schema.register( 'fancyParagraph', {
-				inheritAllFrom: 'paragraph'
-			} );
-
-			elementToElement( conversion, {
-				model: 'fancyParagraph',
-				view: {
-					name: 'p',
-					class: 'fancy'
-				}
-			} );
-
-			test( '<p class="fancy">Foo</p>', '<fancyParagraph>Foo</fancyParagraph>' );
-		} );
-
-		it( 'config.view is an object with upcastAlso defined', () => {
-			elementToElement( conversion, {
-				model: 'paragraph',
-				view: 'p',
-				upcastAlso: [
-					'div',
-					{
-						// Match any name.
-						name: /./,
-						style: {
-							display: 'block'
-						}
-					}
-				]
-			} );
-
-			test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
-			test( '<div>Foo</div>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
-			test( '<span style="display:block">Foo</span>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
-		} );
-
-		it( 'upcastAlso given as a function', () => {
-			schema.register( 'heading', {
-				inheritAllFrom: '$block'
-			} );
-
-			elementToElement( conversion, {
-				model: 'heading',
-				view: 'h2',
-				upcastAlso: viewElement => {
-					const fontSize = viewElement.getStyle( 'font-size' );
-
-					if ( !fontSize ) {
-						return null;
-					}
-
-					const match = fontSize.match( /(\d+)\s*px/ );
-
-					if ( !match ) {
-						return null;
-					}
-
-					const size = Number( match[ 1 ] );
-
-					if ( size >= 26 ) {
-						return { name: true, style: [ 'font-size' ] };
-					}
-
-					return null;
-				}
-			} );
-
-			elementToElement( conversion, {
-				model: 'paragraph',
-				view: 'p'
-			} );
-
-			test( '<p></p>', '<paragraph></paragraph>' );
-			test( '<p style="font-size:20px"></p>', '<paragraph></paragraph>', '<p></p>' );
-
-			test( '<h2></h2>', '<heading></heading>' );
-			test( '<p style="font-size:26px"></p>', '<heading></heading>', '<h2></h2>' );
-		} );
-	} );
-
-	describe( 'attributeToElement', () => {
-		beforeEach( () => {
-			elementToElement( conversion, { model: 'paragraph', view: 'p' } );
-		} );
-
-		it( 'config.view is a string', () => {
-			attributeToElement( conversion, 'bold', { view: 'strong' } );
-
-			test( '<p><strong>Foo</strong> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
-		} );
-
-		it( 'config.view is an object', () => {
-			attributeToElement( conversion, 'bold', {
-				view: {
-					name: 'span',
-					class: 'bold'
-				}
-			} );
-
-			test( '<p><span class="bold">Foo</span> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
-		} );
-
-		it( 'config.view is an object with upcastAlso defined', () => {
-			attributeToElement( conversion, 'bold', {
-				view: 'strong',
-				upcastAlso: [
-					'b',
-					{
-						name: 'span',
-						class: 'bold'
-					},
-					{
-						name: 'span',
-						style: {
-							'font-weight': 'bold'
-						}
-					},
-					viewElement => {
-						const fontWeight = viewElement.getStyle( 'font-weight' );
-
-						if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test( fontWeight ) && Number( fontWeight ) > 500 ) {
-							return {
-								name: true,
-								style: [ 'font-weight' ]
-							};
-						}
-					}
-				]
-			} );
-
-			test(
-				'<p><strong>Foo</strong></p>',
-				'<paragraph><$text bold="true">Foo</$text></paragraph>'
-			);
-
-			test(
-				'<p><b>Foo</b></p>',
-				'<paragraph><$text bold="true">Foo</$text></paragraph>',
-				'<p><strong>Foo</strong></p>'
-			);
-
-			test(
-				'<p><span class="bold">Foo</span></p>',
-				'<paragraph><$text bold="true">Foo</$text></paragraph>',
-				'<p><strong>Foo</strong></p>'
-			);
-
-			test(
-				'<p><span style="font-weight: bold;">Foo</span></p>',
-				'<paragraph><$text bold="true">Foo</$text></paragraph>',
-				'<p><strong>Foo</strong></p>'
-			);
-
-			test(
-				'<p><span style="font-weight: 500;">Foo</span></p>',
-				'<paragraph>Foo</paragraph>',
-				'<p>Foo</p>'
-			);
-
-			test(
-				'<p><span style="font-weight: 600;">Foo</span></p>',
-				'<paragraph><$text bold="true">Foo</$text></paragraph>',
-				'<p><strong>Foo</strong></p>'
-			);
-		} );
-
-		it( 'config.model is a string', () => {
-			schema.extend( '$text', {
-				allowAttributes: [ 'styled' ]
-			} );
-
-			attributeToElement( conversion, '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', () => {
-			schema.extend( '$text', {
-				allowAttributes: [ 'fontSize' ]
-			} );
-
-			attributeToElement( conversion, 'fontSize', [
-				{
-					model: 'big',
-					view: {
-						name: 'span',
-						style: {
-							'font-size': '1.2em'
-						}
-					}
-				},
-				{
-					model: 'small',
-					view: {
-						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' ]
-			} );
-
-			attributeToElement( conversion, 'fontSize', [
-				{
-					model: 'big',
-					view: {
-						name: 'span',
-						style: {
-							'font-size': '1.2em'
-						}
-					},
-					upcastAlso: viewElement => {
-						const fontSize = viewElement.getStyle( 'font-size' );
-
-						if ( !fontSize ) {
-							return null;
-						}
-
-						const match = fontSize.match( /(\d+)\s*px/ );
-
-						if ( !match ) {
-							return null;
-						}
-
-						const size = Number( match[ 1 ] );
-
-						if ( viewElement.is( 'span' ) && size > 10 ) {
-							return { name: true, style: [ 'font-size' ] };
-						}
-
-						return null;
-					}
-				},
-				{
-					model: 'small',
-					view: {
-						name: 'span',
-						style: {
-							'font-size': '0.8em'
-						}
-					},
-					upcastAlso: viewElement => {
-						const fontSize = viewElement.getStyle( 'font-size' );
-
-						if ( !fontSize ) {
-							return null;
-						}
-
-						const match = fontSize.match( /(\d+)\s*px/ );
-
-						if ( !match ) {
-							return null;
-						}
-
-						const size = Number( match[ 1 ] );
-
-						if ( viewElement.is( 'span' ) && size < 10 ) {
-							return { name: true, style: [ 'font-size' ] };
-						}
-
-						return null;
-					}
-				}
-			] );
-
-			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:12px">Foo</span> bar</p>',
-				'<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>',
-				'<p><span style="font-size:1.2em">Foo</span> bar</p>'
-			);
-
-			test(
-				'<p><span style="font-size:0.8em">Foo</span> bar</p>',
-				'<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>'
-			);
-
-			test(
-				'<p><span style="font-size:8px">Foo</span> bar</p>',
-				'<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>',
-				'<p><span style="font-size:0.8em">Foo</span> bar</p>'
-			);
-
-			test(
-				'<p><span style="font-size:10px">Foo</span> bar</p>',
-				'<paragraph>Foo bar</paragraph>',
-				'<p>Foo bar</p>'
-			);
-		} );
-	} );
-
-	describe( 'attributeToAttribute', () => {
-		beforeEach( () => {
-			elementToElement( conversion, { model: 'image', view: 'img' } );
-
-			schema.register( 'image', {
-				inheritAllFrom: '$block',
-			} );
-		} );
-
-		it( 'config is not set', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'src' ]
-			} );
-
-			attributeToAttribute( conversion, 'src' );
-
-			test( '<img src="foo.jpg"></img>', '<image src="foo.jpg"></image>' );
-		} );
-
-		it( 'config.view is a string', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'source' ]
-			} );
-
-			attributeToAttribute( conversion, 'source', { view: 'src' } );
-
-			test( '<img src="foo.jpg"></img>', '<image source="foo.jpg"></image>' );
-		} );
-
-		it( 'config.view is an object', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'aside' ]
-			} );
-
-			attributeToAttribute( conversion, 'aside', {
-				model: true,
-				view: {
-					name: 'img',
-					key: 'class',
-					value: 'aside half-size'
-				}
-			} );
-
-			elementToElement( conversion, { model: 'paragraph', view: 'p' } );
-
-			test( '<img class="aside half-size"></img>', '<image aside="true"></image>' );
-			test( '<p class="aside half-size"></p>', '<paragraph></paragraph>', '<p></p>' );
-		} );
-
-		it( 'config is an array', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'styled' ]
-			} );
-
-			attributeToAttribute( conversion, 'styled', [
-				{
-					model: 'dark',
-					view: {
-						key: 'class',
-						value: 'styled styled-dark'
-					}
-				},
-				{
-					model: 'light',
-					view: {
-						key: 'class',
-						value: 'styled styled-light'
-					}
-				}
-			] );
-
-			test( '<img class="styled styled-dark"></img>', '<image styled="dark"></image>' );
-			test( '<img class="styled styled-light"></img>', '<image styled="light"></image>' );
-		} );
-
-		it( 'config is an array with upcastAlso defined', () => {
-			elementToElement( conversion, { model: 'paragraph', view: 'p' } );
-
-			schema.extend( 'paragraph', {
-				allowAttributes: [ 'align' ]
-			} );
-
-			attributeToAttribute( conversion, 'align', [
-				{
-					model: 'right',
-					view: {
-						key: 'class',
-						value: 'align-right'
-					},
-					upcastAlso: viewElement => {
-						if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
-							return {
-								style: [ 'text-align' ]
-							};
-						}
-
-						return null;
-					}
-				},
-				{
-					model: 'center',
-					view: {
-						key: 'class',
-						value: 'align-center'
-					},
-					upcastAlso: {
-						style: {
-							'text-align': 'center'
-						}
-					}
-				}
-			] );
-
-			test(
-				'<p class="align-right">Foo</p>',
-				'<paragraph align="right">Foo</paragraph>'
-			);
-
-			test(
-				'<p style="text-align:right">Foo</p>',
-				'<paragraph align="right">Foo</paragraph>',
-				'<p class="align-right">Foo</p>'
-			);
-
-			test(
-				'<p class="align-center">Foo</p>',
-				'<paragraph align="center">Foo</paragraph>'
-			);
-
-			test(
-				'<p style="text-align:center">Foo</p>',
-				'<paragraph align="center">Foo</paragraph>',
-				'<p class="align-center">Foo</p>'
-			);
-		} );
-	} );
-
-	function test( input, expectedModel, expectedView = null ) {
-		loadData( input );
-
-		expect( modelStringify( model.document.getRoot() ) ).to.equal( expectedModel );
-		expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( expectedView || input );
-	}
-
-	function loadData( input ) {
-		const parsedView = viewParse( input );
-
-		model.change( writer => {
-			const convertedModel = upcastDispatcher.convert( parsedView, writer );
-			writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, modelRoot.maxOffset ) );
-			writer.insert( convertedModel, modelRoot, 0 );
-		} );
-	}
-} );