Procházet zdrojové kódy

Changed: Improvements in view-to-model helpers.

Szymon Cofalik před 7 roky
rodič
revize
239f9098d8

+ 120 - 47
packages/ckeditor5-engine/src/conversion/definition-conversion.js

@@ -34,24 +34,53 @@ import {
  *		} );
  *
  *		modelElementIsViewElement( conversion, {
- *			model: 'blockquote',
- *			view: 'blockquote',
- *			alternativeView: [
+ *			model: 'paragraph',
+ *			view: 'p',
+ *			upcastAlso: [
+ *				'div',
  *				{
- *					name: 'div',
- *					class: 'blockquote'
+ *					// Match any name.
+ *					name: /./,
+ *					style: {
+ *						display: 'block'
+ *					}
  *				}
  *			]
  *		} );
  *
+ *		modelElementIsViewElement( 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 {String|module:engine/view/viewelementdefinition~ViewElementDefinition} definition.view Name or a definition of
- * a view element to convert.
- * @param {Array.<String|module:engine/view/viewelementdefinition~ViewElementDefinition>} [definition.alternativeView]
- * Alternative forms of view, that also should be converted to model. Keep in mind that those will be "converted back"
- * to the main form, given in `definition.view`.
+ * @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 modelElementIsViewElement( conversion, definition ) {
 	// Set model-to-view conversion.
@@ -81,7 +110,7 @@ export function modelElementIsViewElement( conversion, definition ) {
  *
  *		modelAttributeIsViewElement( conversion, 'bold', {
  *			view: 'strong',
- *			alternativeView: [
+ *			upcastAlso: [
  *				'b',
  *				{
  *					name: 'span',
@@ -92,6 +121,16 @@ export function modelElementIsViewElement( conversion, definition ) {
  *					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' ]
+ *						};
+ *					}
  *				}
  *			]
  *		} );
@@ -134,14 +173,27 @@ export function modelElementIsViewElement( conversion, definition ) {
  *						'font-size': '1.2em'
  *					}
  *				},
- *				alternativeView: [
- *					{
- *						name: 'span',
- *						style: {
- *							'font-size': '12px'
- *						}
+ *				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',
@@ -151,14 +203,27 @@ export function modelElementIsViewElement( conversion, definition ) {
  *						'font-size': '0.8em'
  *					}
  *				},
- *				alternativeView: [
- *					{
- *						name: 'span',
- *						style: {
- *							'font-size': '8px'
- *						}
+ *				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;
+ *				}
  *			}
  *		] );
  *
@@ -168,11 +233,9 @@ export function modelElementIsViewElement( conversion, definition ) {
  * @param {*} [definition.model] The value of the converted model attribute. If omitted, in model-to-view conversion,
  * the item will be treated as a default item, that will be used when no other item matches. In view-to-model conversion,
  * the model attribute value will be set to `true`.
- * @param {String|module:engine/view/viewelementdefinition~ViewElementDefinition} definition.view Name or a definition of
- * a view element to convert.
- * @param {Array.<String|module:engine/view/viewelementdefinition~ViewElementDefinition>} [definition.alternativeView]
- * Alternative forms of view, that also should be converted to model. Keep in mind that those will be "converted back"
- * to the main form, given in `definition.view`.
+ * @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 modelAttributeIsViewElement( conversion, modelAttributeKey, definition ) {
 	// Set model-to-view conversion.
@@ -206,6 +269,7 @@ export function modelAttributeIsViewElement( conversion, modelAttributeKey, defi
  *		modelAttributeIsViewAttribute( conversion, 'aside', {
  *			model: true,
  *			view: {
+ *				name: 'img',
  *				key: 'class',
  *				value: 'aside half-size'
  *			}
@@ -235,12 +299,15 @@ export function modelAttributeIsViewElement( conversion, modelAttributeKey, defi
  *					key: 'class',
  *					value: 'align-right'
  *				},
- *				alternativeView: [
- *					{
- *						key: 'style',
- *						value: 'text-align:right;'
+ *				upcastAlso: viewElement => {
+ *					if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
+ *						return {
+ *							style: [ 'text-align' ]
+ *						};
  *					}
- *				]
+ *
+ *					return null;
+ *				}
  *			},
  *			{
  *				model: 'center',
@@ -248,12 +315,11 @@ export function modelAttributeIsViewElement( conversion, modelAttributeKey, defi
  *					key: 'class',
  *					value: 'align-center'
  *				},
- *				alternativeView: [
- *					{
- *						key: 'style',
- *						value: 'text-align:center;'
+ *				upcastAlso: {
+ *					style: {
+ *						'text-align': 'center'
  *					}
- *				]
+ *				}
  *			}
  *		] );
  *
@@ -265,11 +331,12 @@ export function modelAttributeIsViewElement( conversion, modelAttributeKey, defi
  * @param {*} [definition.model] The value of the converted model attribute. If omitted, in model-to-view conversion,
  * the item will be treated as a default item, that will be used when no other item matches. In view-to-model conversion,
  * the model attribute value will be set to the same value as in the view.
- * @param {String|module:engine/view/viewelementdefinition~ViewElementDefinition} definition.view Name or a definition of
- * a view element to convert.
- * @param {Array.<String|module:engine/view/viewelementdefinition~ViewElementDefinition>} [definition.alternativeView]
- * Alternative forms of view, that also should be converted to model. Keep in mind that those will be "converted back"
- * to the main form, given in `definition.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 modelAttributeIsViewAttribute( conversion, modelAttributeKey, definition ) {
 	// Set model-to-view conversion.
@@ -313,10 +380,16 @@ function _getModelAttributeDefinition( key, model ) {
 }
 
 // Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
-// `definition.alternativeView`.
+// `definition.upcastAlso`.
 //
 // @param {Object} definition Conversion definition.
 // @returns {Array} Array containing view definitions.
 function _getAllViews( definition ) {
-	return [].concat( definition.view ).concat( definition.alternativeView || [] );
+	if ( !definition.upcastAlso ) {
+		return [ definition.view ];
+	} else {
+		const upcastAlso = Array.isArray( definition.upcastAlso ) ? definition.upcastAlso : [ definition.upcastAlso ];
+
+		return [ definition.view ].concat( upcastAlso );
+	}
 }

+ 92 - 24
packages/ckeditor5-engine/src/conversion/view-to-model-helpers.js

@@ -52,8 +52,7 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
  * @param {Object} config Conversion configuration.
- * @param {String|module:engine/view/viewelementdefinition~ViewElementDefinition} config.view View element name, or
- * a view element definition. Conversion will be set for view elements which match the name or the definition.
+ * @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.
@@ -63,10 +62,12 @@ export function elementToElement( config, priority = 'normal' ) {
 	config = cloneDeep( config );
 
 	const converter = _prepareToElementConverter( config );
-	const elementName = typeof config.view == 'string' ? config.view : config.view.name;
+
+	const elementName = _getViewElementNameFromConfig( config );
+	const eventName = elementName ? 'element:' + elementName : 'element';
 
 	return dispatcher => {
-		dispatcher.on( 'element:' + elementName, converter, { priority } );
+		dispatcher.on( eventName, converter, { priority } );
 	};
 }
 
@@ -128,8 +129,7 @@ export function elementToElement( config, priority = 'normal' ) {
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
  * @param {Object} config Conversion configuration.
- * @param {String|module:engine/view/viewelementdefinition~ViewElementDefinition} config.view View element name, or
- * a view element definition. Conversion will be set for view elements which match the name or the definition.
+ * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  * @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`.
@@ -141,10 +141,13 @@ export function elementToAttribute( config, priority = 'low' ) {
 
 	_normalizeModelAttributeConfig( config );
 
-	const converter = _prepareToAttributeConverter( config );
+	const converter = _prepareToAttributeConverter( config, false );
+
+	const elementName = _getViewElementNameFromConfig( config );
+	const eventName = elementName ? 'element:' + elementName : 'element';
 
 	return dispatcher => {
-		dispatcher.on( 'element', converter, { priority } );
+		dispatcher.on( eventName, converter, { priority } );
 	};
 }
 
@@ -158,18 +161,21 @@ export function elementToAttribute( config, priority = 'low' ) {
  *
  *		attributeToAttribute( { view: 'src', model: 'source' } );
  *
- *		attributeToAttribute( { view: 'src', model: 'source' }, 'normal' );
+ *		attributeToAttribute( { view: { key: 'src' }, model: 'source' } );
+ *
+ *		attributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
  *
  *		attributeToAttribute( {
  *			view: {
- *				key: 'class',
- *				value: 'styled'
+ *				key: 'data-style',
+ *				value: /[\s\S]+/
  *			},
  *			model: 'styled'
  *		} );
  *
  *		attributeToAttribute( {
  *			view: {
+ *				name: 'span',
  *				key: 'class',
  *				value: 'styled-dark'
  *			},
@@ -199,8 +205,11 @@ export function elementToAttribute( config, priority = 'low' ) {
  *
  * @param {String|Object} config Conversion configuration. If given as a `String`, the conversion will be set for a
  * view attribute with given key. The model attribute key and value will be same as view attribute key and value.
- * @param {String|Object} config.view View attribute key or an object with `key` and `value` properties.
- * Conversion will be set for view attributes with given key or which match given object.
+ * @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed,
+ * attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property,
+ * specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name`
+ * property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`,
+ * a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`.
  * @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.
@@ -210,10 +219,15 @@ export function elementToAttribute( config, priority = 'low' ) {
 export function attributeToAttribute( config, priority = 'low' ) {
 	config = cloneDeep( config );
 
-	const viewKey = _normalizeFromAttributeConfig( config );
+	let viewKey = null;
+
+	if ( typeof config.view == 'string' || config.view.key ) {
+		viewKey = _normalizeViewAttributeKeyValueConfig( config );
+	}
+
 	_normalizeModelAttributeConfig( config, viewKey );
 
-	const converter = _prepareToAttributeConverter( config );
+	const converter = _prepareToAttributeConverter( config, true );
 
 	return dispatcher => {
 		dispatcher.on( 'element', converter, { priority } );
@@ -247,8 +261,7 @@ export function attributeToAttribute( config, priority = 'low' ) {
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
  * @param {Object} config Conversion configuration.
- * @param {String|module:engine/view/viewelementdefinition~ViewElementDefinition} config.view View element name, or
- * a view element definition. Conversion will be set for view elements which match the name or the definition.
+ * @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.
@@ -262,6 +275,23 @@ export function elementToMarker( config, priority = 'normal' ) {
 	return elementToElement( config, priority );
 }
 
+// Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name
+// and if so, returns it.
+//
+// @param {Object} config Conversion config.
+// @returns {String|null} View element name or `null` if name is not directly set.
+function _getViewElementNameFromConfig( config ) {
+	if ( typeof config.view == 'string' ) {
+		return config.view;
+	}
+
+	if ( typeof config.view == 'object' && typeof config.view.name == 'string' ) {
+		return config.view.name;
+	}
+
+	return null;
+}
+
 // Helper for to-model-element conversion. Takes a config object and returns a proper converter function.
 //
 // @param {Object} config Conversion configuration.
@@ -352,17 +382,42 @@ function _getModelElement( model, input, writer ) {
 	}
 }
 
-function _normalizeFromAttributeConfig( config ) {
-	const key = typeof config.view == 'string' ? config.view : config.view.key;
-	const value = typeof config.view == 'string' ? /[\s\S]*/ : config.view.value;
+// Helper function view-attribute-to-model-attribute helper. Normalizes `config.view` which was set as `String` or
+// as an `Object` with `key`, `value` and `name` properties. Normalized `config.view` has is compatible with
+// {@link module:engine/view/matcher~MatcherPattern}.
+//
+// @param {Object} config Conversion config.
+// @returns {String} Key of the converted view attribute.
+function _normalizeViewAttributeKeyValueConfig( config ) {
+	if ( typeof config.view == 'string' ) {
+		config.view = { key: config.view };
+	}
+
+	const key = config.view.key;
+	const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
+
+	const normalized = {
+		attribute: {
+			[ key ]: value
+		}
+	};
+
+	if ( config.view.name ) {
+		normalized.name = config.view.name;
+	}
 
-	config.view = { attribute: {
-		[ key ]: value
-	} };
+	config.view = normalized;
 
 	return key;
 }
 
+// Helper function that normalizes `config.model` in from-model-attribute conversion. `config.model` can be set
+// as a `String`, an `Object` with only `key` property or an `Object` with `key` and `value` properties. Normalized
+// `config.model` is an `Object` with `key` and `value` properties.
+//
+// @param {Object} config Conversion config.
+// @param {String} viewAttributeKeyToCopy Key of the  converted view attribute. If it is set, model attribute value
+// will be equal to view attribute value.
 function _normalizeModelAttributeConfig( config, viewAttributeKeyToCopy = null ) {
 	const defaultModelValue = viewAttributeKeyToCopy === null ? true : viewElement => viewElement.getAttribute( viewAttributeKeyToCopy );
 
@@ -377,7 +432,8 @@ function _normalizeModelAttributeConfig( config, viewAttributeKeyToCopy = null )
 //
 // @param {String} modelAttributeKey The key of the model attribute to set on a model node.
 // @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
-function _prepareToAttributeConverter( config ) {
+// @param {Boolean} consumeName If set to `true` converter will not consume element's name.
+function _prepareToAttributeConverter( config, consumeName ) {
 	const matcher = new Matcher( config.view );
 
 	return ( evt, data, conversionApi ) => {
@@ -396,6 +452,11 @@ function _prepareToAttributeConverter( config ) {
 			return;
 		}
 
+		if ( !consumeName ) {
+			// Do not test or consume `name` consumable.
+			delete match.match.name;
+		}
+
 		// Try to consume appropriate values from consumable values list.
 		if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) {
 			return;
@@ -417,6 +478,13 @@ function _prepareToAttributeConverter( config ) {
 	};
 }
 
+// Helper function for to-model-attribute converter. Sets model attribute on given range. Checks {@link module:engine/model/schema~Schema}
+// to ensure proper model structure.
+//
+// @param {module:engine/model/range~Range} modelRange Model range on which attribute should be set.
+// @param {Object} modelAttribute Model attribute to set.
+// @param {Object} conversionApi Conversion API.
+// @returns {Boolean} `true` if attribute was set on at least one node from given `modelRange`.
 function _setAttributeOn( modelRange, modelAttribute, conversionApi ) {
 	let result = false;
 

+ 144 - 39
packages/ckeditor5-engine/tests/conversion/definition-conversion.js

@@ -78,24 +78,68 @@ describe( 'definition-conversion', () => {
 			test( '<p class="fancy">Foo</p>', '<fancyParagraph>Foo</fancyParagraph>' );
 		} );
 
-		it( 'config.view is an object with alternative view', () => {
-			schema.register( 'blockquote', {
+		it( 'config.view is an object with upcastAlso defined', () => {
+			modelElementIsViewElement( 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'
 			} );
 
 			modelElementIsViewElement( conversion, {
-				model: 'blockquote',
-				view: 'blockquote',
-				alternativeView: [
-					{
-						name: 'div',
-						class: 'blockquote'
+				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;
+				}
+			} );
+
+			modelElementIsViewElement( conversion, {
+				model: 'paragraph',
+				view: 'p'
 			} );
 
-			test( '<blockquote>Foo</blockquote>', '<blockquote>Foo</blockquote>' );
-			test( '<div class="blockquote">Foo</div>', '<blockquote>Foo</blockquote>', '<blockquote>Foo</blockquote>' );
+			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>' );
 		} );
 	} );
 
@@ -121,10 +165,10 @@ describe( 'definition-conversion', () => {
 			test( '<p><span class="bold">Foo</span> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
 		} );
 
-		it( 'config.view is an object with alternative view', () => {
+		it( 'config.view is an object with upcastAlso defined', () => {
 			modelAttributeIsViewElement( conversion, 'bold', {
 				view: 'strong',
-				alternativeView: [
+				upcastAlso: [
 					'b',
 					{
 						name: 'span',
@@ -135,6 +179,16 @@ describe( 'definition-conversion', () => {
 						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' ]
+							};
+						}
 					}
 				]
 			} );
@@ -161,6 +215,18 @@ describe( 'definition-conversion', () => {
 				'<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', () => {
@@ -216,7 +282,7 @@ describe( 'definition-conversion', () => {
 			);
 		} );
 
-		it( 'config is an array with alternative view', () => {
+		it( 'config is an array with upcastAlso defined', () => {
 			schema.extend( '$text', {
 				allowAttributes: [ 'fontSize' ]
 			} );
@@ -230,14 +296,27 @@ describe( 'definition-conversion', () => {
 							'font-size': '1.2em'
 						}
 					},
-					alternativeView: [
-						{
-							name: 'span',
-							style: {
-								'font-size': '12px'
-							}
+					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',
@@ -247,14 +326,27 @@ describe( 'definition-conversion', () => {
 							'font-size': '0.8em'
 						}
 					},
-					alternativeView: [
-						{
-							name: 'span',
-							style: {
-								'font-size': '8px'
-							}
+					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;
+					}
 				}
 			] );
 
@@ -279,6 +371,12 @@ describe( 'definition-conversion', () => {
 				'<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>'
+			);
 		} );
 	} );
 
@@ -319,12 +417,16 @@ describe( 'definition-conversion', () => {
 			modelAttributeIsViewAttribute( conversion, 'aside', {
 				model: true,
 				view: {
+					name: 'img',
 					key: 'class',
 					value: 'aside half-size'
 				}
 			} );
 
+			modelElementIsViewElement( 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', () => {
@@ -353,8 +455,9 @@ describe( 'definition-conversion', () => {
 			test( '<img class="styled styled-light"></img>', '<image styled="light"></image>' );
 		} );
 
-		it( 'config is an array with alternative view', () => {
+		it( 'config is an array with upcastAlso defined', () => {
 			modelElementIsViewElement( conversion, { model: 'paragraph', view: 'p' } );
+
 			schema.extend( 'paragraph', {
 				allowAttributes: [ 'align' ]
 			} );
@@ -366,12 +469,15 @@ describe( 'definition-conversion', () => {
 						key: 'class',
 						value: 'align-right'
 					},
-					alternativeView: [
-						{
-							key: 'style',
-							value: 'text-align:right;'
+					upcastAlso: viewElement => {
+						if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
+							return {
+								style: [ 'text-align' ]
+							};
 						}
-					]
+
+						return null;
+					}
 				},
 				{
 					model: 'center',
@@ -379,12 +485,11 @@ describe( 'definition-conversion', () => {
 						key: 'class',
 						value: 'align-center'
 					},
-					alternativeView: [
-						{
-							key: 'style',
-							value: 'text-align:center;'
+					upcastAlso: {
+						style: {
+							'text-align': 'center'
 						}
-					]
+					}
 				}
 			] );
 

+ 22 - 0
packages/ckeditor5-engine/tests/conversion/model-to-view-converters.js

@@ -798,6 +798,28 @@ describe( 'model-to-view-converters', () => {
 
 				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
 			} );
+
+			it( 'should do nothing if collapsed marker is converted', () => {
+				const descriptor = { class: 'foo' };
+
+				dispatcher.on( 'addMarker:marker', highlightText( descriptor ), { priority: 'high' } );
+				dispatcher.on( 'addMarker:marker', highlightElement( descriptor ), { priority: 'high' } );
+				dispatcher.on( 'removeMarker:marker', removeHighlight( descriptor ), { priority: 'high' } );
+
+				markerRange = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 0 );
+
+				model.change( () => {
+					model.markers.set( 'marker', markerRange );
+				} );
+
+				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
+
+				model.change( () => {
+					model.markers.remove( 'marker' );
+				} );
+
+				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
+			} );
 		} );
 
 		describe( 'on element', () => {

+ 40 - 9
packages/ckeditor5-engine/tests/conversion/view-to-model-helpers.js

@@ -346,13 +346,28 @@ describe( 'view-to-model-helpers', () => {
 			);
 		} );
 
+		it( 'config.view has only key set', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'source' ]
+			} );
+
+			const helper = attributeToAttribute( { view: { key: 'src' }, model: 'source' } );
+
+			conversion.for( 'view' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
+				'<image source="foo.jpg"></image>'
+			);
+		} );
+
 		it( 'can be overwritten using priority', () => {
 			schema.extend( 'image', {
 				allowAttributes: [ 'src', 'source' ]
 			} );
 
-			const helperA = attributeToAttribute( { view: 'src', model: 'src' } );
-			const helperB = attributeToAttribute( { view: 'src', model: 'source' }, 'normal' );
+			const helperA = attributeToAttribute( { view: { key: 'src' }, model: 'src' } );
+			const helperB = attributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
 
 			conversion.for( 'view' ).add( helperA ).add( helperB );
 
@@ -362,7 +377,7 @@ describe( 'view-to-model-helpers', () => {
 			);
 		} );
 
-		it( 'config.view is an object', () => {
+		it( 'config.view has value set', () => {
 			schema.extend( 'image', {
 				allowAttributes: [ 'styled' ]
 			} );
@@ -390,6 +405,7 @@ describe( 'view-to-model-helpers', () => {
 
 			const helper = attributeToAttribute( {
 				view: {
+					name: 'img',
 					key: 'class',
 					value: 'styled-dark'
 				},
@@ -399,17 +415,24 @@ describe( 'view-to-model-helpers', () => {
 				}
 			} );
 
-			conversion.for( 'view' ).add( helper );
+			conversion.for( 'view' )
+				.add( helper )
+				.add( elementToElement( { view: 'p', model: 'paragraph' } ) );
 
 			expectResult(
-				new ViewAttributeElement( 'img', { class: 'styled-dark' } ),
+				new ViewContainerElement( 'img', { class: 'styled-dark' } ),
 				'<image styled="dark"></image>'
 			);
 
 			expectResult(
-				new ViewAttributeElement( 'img', { class: 'styled-xxx' } ),
+				new ViewContainerElement( 'img', { class: 'styled-xxx' } ),
 				'<image></image>'
 			);
+
+			expectResult(
+				new ViewContainerElement( 'p', { class: 'styled-dark' } ),
+				'<paragraph></paragraph>'
+			);
 		} );
 
 		it( 'model attribute value is a function', () => {
@@ -418,17 +441,25 @@ describe( 'view-to-model-helpers', () => {
 			} );
 
 			const helper = attributeToAttribute( {
-				view: 'data-style',
+				view: {
+					key: 'class',
+					value: /styled-[\S]+/
+				},
 				model: {
 					key: 'styled',
-					value: viewElement => viewElement.getAttribute( 'data-style' ).substr( 6 )
+					value: viewElement => {
+						const regexp = /styled-([\S]+)/;
+						const match = viewElement.getAttribute( 'class' ).match( regexp );
+
+						return match[ 1 ];
+					}
 				}
 			} );
 
 			conversion.for( 'view' ).add( helper );
 
 			expectResult(
-				new ViewAttributeElement( 'img', { 'data-style': 'style-dark' } ),
+				new ViewAttributeElement( 'img', { 'class': 'styled-dark' } ),
 				'<image styled="dark"></image>'
 			);
 		} );