Przeglądaj źródła

Changed: Merged downcast-helpers.js to downcast-converters.js and upcast-helpers.js to upcast-converters.js.

Szymon Cofalik 7 lat temu
rodzic
commit
a5a9241728

+ 6 - 6
packages/ckeditor5-engine/src/conversion/conversion.js

@@ -63,15 +63,15 @@ export default class Conversion {
 	 *
 	 * For downcast (model to view conversion), these are:
 	 *
-	 * * {@link module:engine/conversion/downcast-helpers~downcastElementToElement downcast element to element helper},
-	 * * {@link module:engine/conversion/downcast-helpers~downcastAttributeToElement downcast attribute to element helper},
-	 * * {@link module:engine/conversion/downcast-helpers~downcastAttributeToAttribute downcast attribute to attribute helper}.
+	 * * {@link module:engine/conversion/downcast-converters~downcastElementToElement downcast element to element converter},
+	 * * {@link module:engine/conversion/downcast-converters~downcastAttributeToElement downcast attribute to element converter},
+	 * * {@link module:engine/conversion/downcast-converters~downcastAttributeToAttribute downcast attribute to attribute converter}.
 	 *
 	 * For upcast (view to model conversion), these are:
 	 *
-	 * * {@link module:engine/conversion/upcast-helpers~upcastElementToElement upcast element to element helper},
-	 * * {@link module:engine/conversion/upcast-helpers~upcastElementToAttribute upcast attribute to element helper},
-	 * * {@link module:engine/conversion/upcast-helpers~upcastAttributeToAttribute upcast attribute to attribute helper}.
+	 * * {@link module:engine/conversion/upcast-converters~upcastElementToElement upcast element to element converter},
+	 * * {@link module:engine/conversion/upcast-converters~upcastElementToAttribute upcast attribute to element converter},
+	 * * {@link module:engine/conversion/upcast-converters~upcastAttributeToAttribute upcast attribute to attribute converter}.
 	 *
 	 * An example of using conversion helpers to convert `paragraph` model element to `p` view element (and back):
 	 *

+ 452 - 0
packages/ckeditor5-engine/src/conversion/downcast-converters.js

@@ -7,6 +7,8 @@ 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 ViewText from '../view/text';
@@ -14,6 +16,8 @@ import ViewRange from '../view/range';
 import viewWriter from '../view/writer';
 import DocumentSelection from '../model/documentselection';
 
+import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
+
 /**
  * Contains downcast (model to view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
  *
@@ -21,6 +25,454 @@ import DocumentSelection from '../model/documentselection';
  */
 
 /**
+ * Model element to view element conversion helper.
+ *
+ * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
+ *
+ *		downcastElementToElement( { model: 'paragraph', view: 'p' } );
+ *
+ *		downcastElementToElement( { model: 'paragraph', view: 'p' }, 'high' );
+ *
+ *		downcastElementToElement( { model: 'paragraph', view: new ViewContainerElement( 'p' ) } );
+ *
+ *		downcastElementToElement( {
+ *			model: 'fancyParagraph',
+ *			view: {
+ *				name: 'p',
+ *				class: 'fancy'
+ *			}
+ *		} );
+ *
+ * 		downcastElementToElement( {
+ * 			model: 'heading',
+ * 			view: modelElement => new ViewContainerElement( '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|
+ * module:engine/view/containerelement~ContainerElement} config.view View element name, or a view element definition,
+ * or a function that takes model element as a parameter and returns a view container element,
+ * or a view container element instance. The view element will be used then in conversion.
+ * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function downcastElementToElement( config, priority = 'normal' ) {
+	config = cloneDeep( config );
+
+	_normalizeToElementConfig( config, ViewContainerElement );
+
+	return dispatcher => {
+		dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority } );
+	};
+}
+
+/**
+ * Model attribute to view element conversion helper.
+ *
+ * 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( 'bold', { view: 'strong' }, 'high' );
+ *
+ *		downcastAttributeToElement( 'bold', { view: new ViewAttributeElement( 'strong' ) } );
+ *
+ *		downcastAttributeToElement( 'bold', {
+ *			view: {
+ *				name: 'span',
+ *				class: 'bold'
+ *			}
+ *		} );
+ *
+ *		downcastAttributeToElement( 'styled', {
+ *			model: 'dark',
+ *			view: {
+ *				name: 'span',
+ *				class: [ 'styled', 'styled-dark' ]
+ *			}
+ *		} );
+ *
+ *		downcastAttributeToElement( 'fontSize', [
+ *			{
+ *				model: 'big',
+ *				view: {
+ *					name: 'span',
+ *					style: {
+ *						'font-size': '1.2em'
+ *					}
+ *				}
+ *			},
+ *			{
+ *				model: 'small',
+ *				view: {
+ *					name: 'span',
+ *					style: {
+ *						'font-size': '0.8em'
+ *					}
+ *				}
+ *			}
+ *		] );
+ *
+ * 		downcastAttributeToElement( 'bold', {
+ * 			view: modelAttributeValue => new ViewAttributeElement( '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|
+ * module:engine/view/attributeelement~AttributeElement} 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, or a view attribute element instance.
+ * The view element will be used then in conversion.
+ * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function downcastAttributeToElement( modelAttributeKey, config, priority = 'normal' ) {
+	config = cloneDeep( config );
+
+	_normalizeToElementConfig( config, ViewAttributeElement );
+
+	const elementCreator = _getCreatorForArrayConfig( config );
+
+	return dispatcher => {
+		dispatcher.on( 'attribute:' + modelAttributeKey, wrap( elementCreator ), { priority } );
+	};
+}
+
+/**
+ * Model attribute to view attribute conversion helper.
+ *
+ * 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( 'source', { view: 'src' }, 'high' );
+ *
+ *		downcastAttributeToAttribute( 'stylish', {
+ *			view: {
+ *				key: 'class',
+ *				value: 'styled'
+ *			}
+ *		} );
+ *
+ *		downcastAttributeToAttribute( 'styled', {
+ *			model: 'dark',
+ *			view: {
+ *				key: 'class',
+ *				value: 'styled styled-dark'
+ *			}
+ *		} );
+ *
+ *		downcastAttributeToAttribute( 'style', [
+ *			{
+ *				model: 'dark',
+ *				view: {
+ *					key: 'class',
+ *					value: 'styled-dark'
+ *				}
+ *			},
+ *			{
+ *				model: 'light',
+ *				view: {
+ *					key: 'class',
+ *					value: 'styled-light'
+ *				}
+ *			}
+ *		] );
+ *
+ *		downcastAttributeToAttribute( 'style', {
+ *			view: attributeValue => ( { key: 'class', value: 'style-' + attributeValue } )
+ *		} );
+ *
+ * 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.
+ * @returns {Function} Conversion helper.
+ */
+export function downcastAttributeToAttribute( modelAttributeKey, config = {}, priority = 'normal' ) {
+	config = cloneDeep( config );
+
+	_normalizeToAttributeConfig( modelAttributeKey, config );
+
+	const elementCreator = _getCreatorForArrayConfig( config );
+
+	return dispatcher => {
+		dispatcher.on( 'attribute:' + modelAttributeKey, changeAttribute( elementCreator ), { priority } );
+	};
+}
+
+/**
+ * Model marker to view element conversion helper.
+ *
+ * This conversion results in creating a view element on the boundaries of the converted marker. If converted marker
+ * is collapsed, only one element is created. For example, model marker set like this `<paragraph>F[oo b]ar</paragraph>`
+ * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
+ *
+ *		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: {
+ *				name: 'span',
+ *				attribute: {
+ *					'data-marker': 'search'
+ *				}
+ *			}
+ *		} );
+ *
+ * 		downcastMarkerToElement( {
+ * 			model: 'search',
+ * 			view: data => {
+ *	 			return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
+ * 			}
+ * 		} );
+ *
+ * If function is passed as `config.view` parameter, it will be used to generate both boundary elements. The function
+ * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
+ * The `data` object properties are passed from
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
+ * `data.isOpening` parameter is passed, which is set to `true` for marker start boundary element, and `false` to
+ * marker end boundary element.
+ *
+ * This kind of conversion is useful for saving data into data base, so it should be used in data conversion pipeline.
+ *
+ * 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 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.
+ * @returns {Function} Conversion helper.
+ */
+export function downcastMarkerToElement( config, priority = 'normal' ) {
+	config = cloneDeep( config );
+
+	_normalizeToElementConfig( config, ViewUIElement );
+
+	return dispatcher => {
+		dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority } );
+		dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority } );
+	};
+}
+
+/**
+ * Model marker to highlight conversion helper.
+ *
+ * This conversion results in creating a highlight on view nodes. For this kind of conversion,
+ * {@link module:engine/conversion/downcast-converters~HighlightDescriptor} should be provided.
+ *
+ * For text nodes, a `span` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes
+ * in the converted marker range. For example, model marker set like this `<paragraph>F[oo b]ar</paragraph>` becomes
+ * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
+ *
+ * {@link module:engine/view/containerelement~ContainerElement} may provide custom way of handling highlight. Most often,
+ * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `span`).
+ * For example, model marker set like this `[<image src="foo.jpg"></image>]` becomes `<img src="foo.jpg" class="comment"></img>`
+ * in the view.
+ *
+ * For container elements, the conversion is two-step. While the converter processes highlight descriptor and passes it
+ * to a container element, it is the container element instance itself which applies values from highlight descriptor.
+ * So, in a sense, converter takes care of stating what should be applied on what, while element decides how to apply that.
+ *
+ *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
+ *
+ *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
+ *
+ * 		downcastMarkerToHighlight( {
+ * 			model: 'comment',
+ * 			view: data => {
+ * 				// Assuming that marker name is in a form of comment:commentType.
+ *	 			const commentType = data.markerName.split( ':' )[ 1 ];
+ *
+ *	 			return {
+ *	 				class: [ 'comment', 'comment-' + commentType ]
+ *	 			};
+ * 			}
+ * 		} );
+ *
+ * If function is passed as `config.view` parameter, it will be used to generate highlight descriptor. The function
+ * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
+ * 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.
+ *
+ * @param {Object} config Conversion configuration.
+ * @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.
+ * @returns {Function} Conversion helper.
+ */
+export function downcastMarkerToHighlight( config, priority = 'normal' ) {
+	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 } );
+	};
+}
+
+// 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.
+//
+// @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;
+	}
+
+	// Build `.view` property.
+	// It is expected to be either creator function or view element instance.
+	if ( typeof config.view == 'string' ) {
+		// If `.view` is a string, create a proper view element instance out of given `ViewElementClass` and name given in `.view`.
+		config.view = new ViewElementClass( config.view );
+	} else if ( typeof config.view == 'object' && !( config.view instanceof ViewElementClass ) ) {
+		// If `.view` is an object, use it to build view element instance.
+		config.view = _createViewElementFromDefinition( config.view, ViewElementClass );
+	}
+	// `.view` can be also a function or already a view element instance.
+	// These are already valid types which don't have to be normalized.
+}
+
+// Creates view element instance from provided viewElementDefinition and class.
+//
+// @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
+// @param {Function} ViewElementClass
+// @returns {module:engine/view/element~Element}
+function _createViewElementFromDefinition( viewElementDefinition, ViewElementClass ) {
+	const element = new ViewElementClass( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+
+	if ( viewElementDefinition.style ) {
+		element.setStyle( viewElementDefinition.style );
+	}
+
+	if ( viewElementDefinition.class ) {
+		const classes = viewElementDefinition.class;
+
+		if ( typeof classes == 'string' ) {
+			element.addClass( classes );
+		} else {
+			element.addClass( ...classes );
+		}
+	}
+
+	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 );
+		}
+
+		return;
+	}
+
+	// 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 } );
+	}
+	// `.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.
+//
+// @param {Object|Array.<Object>} config Config with conversion helper configuration.
+function _getCreatorForArrayConfig( config ) {
+	if ( !Array.isArray( config ) ) {
+		config = [ config ];
+	}
+
+	// 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 => {
+		// 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 ) {
+			// If the entry `.view` is a function, execute it and return the value...
+			if ( typeof matchedConfigEntry.view == 'function' ) {
+				return matchedConfigEntry.view( modelAttributeValue );
+			}
+			// Else, just return `.view`, it should be a view element instance after it got normalized earlier.
+			return matchedConfigEntry.view;
+		}
+
+		return null;
+	};
+}
+
+/**
  * Function factory, creates a converter that converts node insertion changes from the model to the view.
  * The view element that will be added to the view depends on passed parameter. If {@link module:engine/view/element~Element} was passed,
  * it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the

+ 0 - 467
packages/ckeditor5-engine/src/conversion/downcast-helpers.js

@@ -1,467 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import ViewContainerElement from '../view/containerelement';
-import ViewAttributeElement from '../view/attributeelement';
-import ViewUIElement from '../view/uielement';
-
-import {
-	insertElement, wrap, changeAttribute,
-	insertUIElement, removeUIElement, highlightText, highlightElement, removeHighlight
-} from './downcast-converters';
-
-import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
-
-/**
- * @module engine/conversion/downcast-helpers
- */
-
-/**
- * Model element to view element conversion helper.
- *
- * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
- *
- *		downcastElementToElement( { model: 'paragraph', view: 'p' } );
- *
- *		downcastElementToElement( { model: 'paragraph', view: 'p' }, 'high' );
- *
- *		downcastElementToElement( { model: 'paragraph', view: new ViewContainerElement( 'p' ) } );
- *
- *		downcastElementToElement( {
- *			model: 'fancyParagraph',
- *			view: {
- *				name: 'p',
- *				class: 'fancy'
- *			}
- *		} );
- *
- * 		downcastElementToElement( {
- * 			model: 'heading',
- * 			view: modelElement => new ViewContainerElement( '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|
- * module:engine/view/containerelement~ContainerElement} config.view View element name, or a view element definition,
- * or a function that takes model element as a parameter and returns a view container element,
- * or a view container element instance. The view element will be used then in conversion.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
- * @returns {Function} Conversion helper.
- */
-export function downcastElementToElement( config, priority = 'normal' ) {
-	config = cloneDeep( config );
-
-	_normalizeToElementConfig( config, ViewContainerElement );
-
-	return dispatcher => {
-		dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority } );
-	};
-}
-
-/**
- * Model attribute to view element conversion helper.
- *
- * 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( 'bold', { view: 'strong' }, 'high' );
- *
- *		downcastAttributeToElement( 'bold', { view: new ViewAttributeElement( 'strong' ) } );
- *
- *		downcastAttributeToElement( 'bold', {
- *			view: {
- *				name: 'span',
- *				class: 'bold'
- *			}
- *		} );
- *
- *		downcastAttributeToElement( 'styled', {
- *			model: 'dark',
- *			view: {
- *				name: 'span',
- *				class: [ 'styled', 'styled-dark' ]
- *			}
- *		} );
- *
- *		downcastAttributeToElement( 'fontSize', [
- *			{
- *				model: 'big',
- *				view: {
- *					name: 'span',
- *					style: {
- *						'font-size': '1.2em'
- *					}
- *				}
- *			},
- *			{
- *				model: 'small',
- *				view: {
- *					name: 'span',
- *					style: {
- *						'font-size': '0.8em'
- *					}
- *				}
- *			}
- *		] );
- *
- * 		downcastAttributeToElement( 'bold', {
- * 			view: modelAttributeValue => new ViewAttributeElement( '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|
- * module:engine/view/attributeelement~AttributeElement} 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, or a view attribute element instance.
- * The view element will be used then in conversion.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
- * @returns {Function} Conversion helper.
- */
-export function downcastAttributeToElement( modelAttributeKey, config, priority = 'normal' ) {
-	config = cloneDeep( config );
-
-	_normalizeToElementConfig( config, ViewAttributeElement );
-
-	const elementCreator = _getCreatorForArrayConfig( config );
-
-	return dispatcher => {
-		dispatcher.on( 'attribute:' + modelAttributeKey, wrap( elementCreator ), { priority } );
-	};
-}
-
-/**
- * Model attribute to view attribute conversion helper.
- *
- * 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( 'source', { view: 'src' }, 'high' );
- *
- *		downcastAttributeToAttribute( 'stylish', {
- *			view: {
- *				key: 'class',
- *				value: 'styled'
- *			}
- *		} );
- *
- *		downcastAttributeToAttribute( 'styled', {
- *			model: 'dark',
- *			view: {
- *				key: 'class',
- *				value: 'styled styled-dark'
- *			}
- *		} );
- *
- *		downcastAttributeToAttribute( 'style', [
- *			{
- *				model: 'dark',
- *				view: {
- *					key: 'class',
- *					value: 'styled-dark'
- *				}
- *			},
- *			{
- *				model: 'light',
- *				view: {
- *					key: 'class',
- *					value: 'styled-light'
- *				}
- *			}
- *		] );
- *
- *		downcastAttributeToAttribute( 'style', {
- *			view: attributeValue => ( { key: 'class', value: 'style-' + attributeValue } )
- *		} );
- *
- * 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.
- * @returns {Function} Conversion helper.
- */
-export function downcastAttributeToAttribute( modelAttributeKey, config = {}, priority = 'normal' ) {
-	config = cloneDeep( config );
-
-	_normalizeToAttributeConfig( modelAttributeKey, config );
-
-	const elementCreator = _getCreatorForArrayConfig( config );
-
-	return dispatcher => {
-		dispatcher.on( 'attribute:' + modelAttributeKey, changeAttribute( elementCreator ), { priority } );
-	};
-}
-
-/**
- * Model marker to view element conversion helper.
- *
- * This conversion results in creating a view element on the boundaries of the converted marker. If converted marker
- * is collapsed, only one element is created. For example, model marker set like this `<paragraph>F[oo b]ar</paragraph>`
- * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
- *
- *		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: {
- *				name: 'span',
- *				attribute: {
- *					'data-marker': 'search'
- *				}
- *			}
- *		} );
- *
- * 		downcastMarkerToElement( {
- * 			model: 'search',
- * 			view: data => {
- *	 			return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
- * 			}
- * 		} );
- *
- * If function is passed as `config.view` parameter, it will be used to generate both boundary elements. The function
- * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
- * The `data` object properties are passed from
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
- * `data.isOpening` parameter is passed, which is set to `true` for marker start boundary element, and `false` to
- * marker end boundary element.
- *
- * This kind of conversion is useful for saving data into data base, so it should be used in data conversion pipeline.
- *
- * 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 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.
- * @returns {Function} Conversion helper.
- */
-export function downcastMarkerToElement( config, priority = 'normal' ) {
-	config = cloneDeep( config );
-
-	_normalizeToElementConfig( config, ViewUIElement );
-
-	return dispatcher => {
-		dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority } );
-		dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority } );
-	};
-}
-
-/**
- * Model marker to highlight conversion helper.
- *
- * This conversion results in creating a highlight on view nodes. For this kind of conversion,
- * {@link module:engine/conversion/downcast-converters~HighlightDescriptor} should be provided.
- *
- * For text nodes, a `span` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes
- * in the converted marker range. For example, model marker set like this `<paragraph>F[oo b]ar</paragraph>` becomes
- * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
- *
- * {@link module:engine/view/containerelement~ContainerElement} may provide custom way of handling highlight. Most often,
- * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `span`).
- * For example, model marker set like this `[<image src="foo.jpg"></image>]` becomes `<img src="foo.jpg" class="comment"></img>`
- * in the view.
- *
- * For container elements, the conversion is two-step. While the converter processes highlight descriptor and passes it
- * to a container element, it is the container element instance itself which applies values from highlight descriptor.
- * So, in a sense, converter takes care of stating what should be applied on what, while element decides how to apply that.
- *
- *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
- *
- *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
- *
- * 		downcastMarkerToHighlight( {
- * 			model: 'comment',
- * 			view: data => {
- * 				// Assuming that marker name is in a form of comment:commentType.
- *	 			const commentType = data.markerName.split( ':' )[ 1 ];
- *
- *	 			return {
- *	 				class: [ 'comment', 'comment-' + commentType ]
- *	 			};
- * 			}
- * 		} );
- *
- * If function is passed as `config.view` parameter, it will be used to generate highlight descriptor. The function
- * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
- * 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.
- *
- * @param {Object} config Conversion configuration.
- * @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.
- * @returns {Function} Conversion helper.
- */
-export function downcastMarkerToHighlight( config, priority = 'normal' ) {
-	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 } );
-	};
-}
-
-// 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.
-//
-// @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;
-	}
-
-	// Build `.view` property.
-	// It is expected to be either creator function or view element instance.
-	if ( typeof config.view == 'string' ) {
-		// If `.view` is a string, create a proper view element instance out of given `ViewElementClass` and name given in `.view`.
-		config.view = new ViewElementClass( config.view );
-	} else if ( typeof config.view == 'object' && !( config.view instanceof ViewElementClass ) ) {
-		// If `.view` is an object, use it to build view element instance.
-		config.view = _createViewElementFromDefinition( config.view, ViewElementClass );
-	}
-	// `.view` can be also a function or already a view element instance.
-	// These are already valid types which don't have to be normalized.
-}
-
-// Creates view element instance from provided viewElementDefinition and class.
-//
-// @param {module:engine/view/elementdefinition~elementDefinition} viewElementDefinition
-// @param {Function} ViewElementClass
-// @returns {module:engine/view/element~Element}
-function _createViewElementFromDefinition( viewElementDefinition, ViewElementClass ) {
-	const element = new ViewElementClass( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
-
-	if ( viewElementDefinition.style ) {
-		element.setStyle( viewElementDefinition.style );
-	}
-
-	if ( viewElementDefinition.class ) {
-		const classes = viewElementDefinition.class;
-
-		if ( typeof classes == 'string' ) {
-			element.addClass( classes );
-		} else {
-			element.addClass( ...classes );
-		}
-	}
-
-	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 );
-		}
-
-		return;
-	}
-
-	// 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 } );
-	}
-	// `.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.
-//
-// @param {Object|Array.<Object>} config Config with conversion helper configuration.
-function _getCreatorForArrayConfig( config ) {
-	if ( !Array.isArray( config ) ) {
-		config = [ config ];
-	}
-
-	// 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 => {
-		// 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 ) {
-			// If the entry `.view` is a function, execute it and return the value...
-			if ( typeof matchedConfigEntry.view == 'function' ) {
-				return matchedConfigEntry.view( modelAttributeValue );
-			}
-			// Else, just return `.view`, it should be a view element instance after it got normalized earlier.
-			return matchedConfigEntry.view;
-		}
-
-		return null;
-	};
-}

+ 3 - 3
packages/ckeditor5-engine/src/conversion/two-way-helpers.js

@@ -4,20 +4,20 @@
  */
 
 /**
- * @module engine/conversion/two-way-helpers
+ * @module engine/conversion/two-way-converters
  */
 
 import {
 	downcastElementToElement,
 	downcastAttributeToElement,
 	downcastAttributeToAttribute
-} from './downcast-helpers';
+} from './downcast-converters';
 
 import {
 	upcastElementToElement,
 	upcastElementToAttribute,
 	upcastAttributeToAttribute
-} from './upcast-helpers';
+} 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).

+ 510 - 2
packages/ckeditor5-engine/src/conversion/upcast-converters.js

@@ -3,7 +3,12 @@
  * For licensing, see LICENSE.md.
  */
 
-import Range from '../model/range';
+import Matcher from '../view/matcher';
+
+import ModelRange from '../model/range';
+import ModelPosition from '../model/position';
+
+import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
 
 /**
  * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
@@ -13,6 +18,509 @@ import Range from '../model/range';
  */
 
 /**
+ * View element to model element conversion helper.
+ *
+ * This conversion results in creating a model element. For example, view `<p>Foo</p>` becomes `<paragraph>Foo</paragraph>` in the model.
+ *
+ * Keep in mind that the element will be inserted only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
+ *
+ *		upcastElementToElement( { view: 'p', model: 'paragraph' } );
+ *
+ *		upcastElementToElement( { view: 'p', model: 'paragraph' }, 'high' );
+ *
+ *		upcastElementToElement( {
+ *			view: {
+ *				name: 'p',
+ *				class: 'fancy'
+ *			},
+ *			model: 'fancyParagraph'
+ *		} );
+ *
+ *		upcastElementToElement( {
+ *			view: {
+ *				name: 'p',
+ *				class: 'fancy'
+ *			},
+ *			model: new ModelElement( 'p', { fancy: true } )
+ *		} );
+ *
+ *		upcastElementToElement( {
+ * 			view: {
+ *				name: 'p',
+ *				class: 'heading'
+ * 			},
+ * 			model: viewElement => new ModelElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } )
+ * 		} );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @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.
+ * @returns {Function} Conversion helper.
+ */
+export function upcastElementToElement( config, priority = 'normal' ) {
+	config = cloneDeep( config );
+
+	const converter = _prepareToElementConverter( config );
+
+	const elementName = _getViewElementNameFromConfig( config );
+	const eventName = elementName ? 'element:' + elementName : 'element';
+
+	return dispatcher => {
+		dispatcher.on( eventName, converter, { priority } );
+	};
+}
+
+/**
+ * View element to model attribute conversion helper.
+ *
+ * This conversion results in setting an attribute on a model node. For example, view `<strong>Foo</strong>` becomes
+ * `Foo` {@link module:engine/model/text~Text model text node} with `bold` attribute set to `true`.
+ *
+ * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
+ *
+ *		upcastElementToAttribute( { view: 'strong', model: 'bold' } );
+ *
+ *		upcastElementToAttribute( { view: 'strong', model: 'bold' }, 'normal' );
+ *
+ *		upcastElementToAttribute( {
+ *			view: {
+ *				name: 'span',
+ *				class: 'bold'
+ *			},
+ *			model: 'bold'
+ *		} );
+ *
+ *		upcastElementToAttribute( {
+ *			view: {
+ *				name: 'span',
+ *				class: [ 'styled', 'styled-dark' ]
+ *			},
+ *			model: {
+ *				key: 'styled',
+ *				value: 'dark'
+ *			}
+ *		} );
+ *
+ * 		upcastElementToAttribute( {
+ *			view: {
+ *				name: 'span',
+ *				style: {
+ *					'font-size': /[\s\S]+/
+ *				}
+ *			},
+ *			model: {
+ *				key: 'fontSize',
+ *				value: viewElement => {
+ *					const fontSize = viewElement.getStyle( 'font-size' );
+ *					const value = fontSize.substr( 0, fontSize.length - 2 );
+ *
+ *					if ( value <= 10 ) {
+ *						return 'small';
+ *					} else if ( value > 12 ) {
+ *						return 'big';
+ *					}
+ *
+ *					return null;
+ *				}
+ *			}
+ *		} );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @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`.
+ * @param {module:utils/priorities~PriorityString} [priority='low'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function upcastElementToAttribute( config, priority = 'low' ) {
+	config = cloneDeep( config );
+
+	_normalizeModelAttributeConfig( config );
+
+	const converter = _prepareToAttributeConverter( config, true );
+
+	const elementName = _getViewElementNameFromConfig( config );
+	const eventName = elementName ? 'element:' + elementName : 'element';
+
+	return dispatcher => {
+		dispatcher.on( eventName, converter, { priority } );
+	};
+}
+
+/**
+ * View attribute to model attribute conversion helper.
+ *
+ * This conversion results in setting an attribute on a model node. For example, view `<img src="foo.jpg"></img>` becomes
+ * `<image source="foo.jpg"></image>` in the model.
+ *
+ * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
+ *
+ *		upcastAttributeToAttribute( { view: 'src', model: 'source' } );
+ *
+ *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } );
+ *
+ *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
+ *
+ *		upcastAttributeToAttribute( {
+ *			view: {
+ *				key: 'data-style',
+ *				value: /[\s\S]+/
+ *			},
+ *			model: 'styled'
+ *		} );
+ *
+ *		upcastAttributeToAttribute( {
+ *			view: {
+ *				name: 'span',
+ *				key: 'class',
+ *				value: 'styled-dark'
+ *			},
+ *			model: {
+ *				key: 'styled',
+ *				value: 'dark'
+ *			}
+ *		} );
+ *
+ *		upcastAttributeToAttribute( {
+ *			view: {
+ *				key: 'class',
+ *				value: /styled-[\S]+/
+ *			},
+ *			model: {
+ *				key: 'styled'
+ *				value: viewElement => {
+ *					const regexp = /styled-([\S]+)/;
+ *					const match = viewElement.getAttribute( 'class' ).match( regexp );
+ *
+ *					return match[ 1 ];
+ *				}
+ *			}
+ *		} );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @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 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.
+ * @param {module:utils/priorities~PriorityString} [priority='low'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function upcastAttributeToAttribute( config, priority = 'low' ) {
+	config = cloneDeep( config );
+
+	let viewKey = null;
+
+	if ( typeof config.view == 'string' || config.view.key ) {
+		viewKey = _normalizeViewAttributeKeyValueConfig( config );
+	}
+
+	_normalizeModelAttributeConfig( config, viewKey );
+
+	const converter = _prepareToAttributeConverter( config, false );
+
+	return dispatcher => {
+		dispatcher.on( 'element', converter, { priority } );
+	};
+}
+
+/**
+ * View element to model marker conversion helper.
+ *
+ * This conversion results in creating a model marker. For example, if the marker was stored in a view as an element:
+ * `<p>Fo<span data-marker="comment" data-comment-id="7"></span>o</p><p>B<span data-marker="comment" data-comment-id="7"></span>ar</p>`,
+ * after the conversion is done, the marker will be available in
+ * {@link module:engine/model/model~Model#markers model document markers}.
+ *
+ *		upcastElementToMarker( { view: 'marker-search', model: 'search' } );
+ *
+ *		upcastElementToMarker( { view: 'marker-search', model: 'search' }, 'high' );
+ *
+ *		upcastElementToMarker( {
+ *			view: 'marker-search',
+ *			model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
+ *		} );
+ *
+ *		upcastElementToMarker( {
+ *			view: {
+ *				name: 'span',
+ *				attribute: {
+ *					'data-marker': 'search'
+ *				}
+ *			},
+ *			model: 'search'
+ *		} );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @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.
+ * @returns {Function} Conversion helper.
+ */
+export function upcastElementToMarker( config, priority = 'normal' ) {
+	config = cloneDeep( config );
+
+	_normalizeToMarkerConfig( config );
+
+	return upcastElementToElement( 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.
+// @returns {Function} View to model converter.
+function _prepareToElementConverter( config ) {
+	const matcher = new Matcher( config.view );
+
+	return ( evt, data, conversionApi ) => {
+		// This will be usually just one pattern but we support matchers with many patterns too.
+		const match = matcher.match( data.viewItem );
+
+		// If there is no match, this callback should not do anything.
+		if ( !match ) {
+			return;
+		}
+
+		// Create model element basing on config.
+		const modelElement = _getModelElement( config.model, data.viewItem, conversionApi.writer );
+
+		// Do not convert if element building function returned falsy value.
+		if ( !modelElement ) {
+			return;
+		}
+
+		// When element was already consumed then skip it.
+		if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) {
+			return;
+		}
+
+		// Find allowed parent for element that we are going to insert.
+		// If current parent does not allow to insert element but one of the ancestors does
+		// then split nodes to allowed parent.
+		const splitResult = conversionApi.splitToAllowedParent( modelElement, data.cursorPosition );
+
+		// When there is no split result it means that we can't insert element to model tree, so let's skip it.
+		if ( !splitResult ) {
+			return;
+		}
+
+		// Insert element on allowed position.
+		conversionApi.writer.insert( modelElement, splitResult.position );
+
+		// Convert children and insert to element.
+		const childrenResult = conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( modelElement ) );
+
+		// Consume appropriate value from consumable values list.
+		conversionApi.consumable.consume( data.viewItem, match.match );
+
+		// Set conversion result range.
+		data.modelRange = new ModelRange(
+			// Range should start before inserted element
+			ModelPosition.createBefore( modelElement ),
+			// Should end after but we need to take into consideration that children could split our
+			// element, so we need to move range after parent of the last converted child.
+			// before: <allowed>[]</allowed>
+			// after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
+			ModelPosition.createAfter( childrenResult.cursorPosition.parent )
+		);
+
+		// Now we need to check where the cursorPosition should be.
+		// If we had to split parent to insert our element then we want to continue conversion inside split parent.
+		//
+		// before: <allowed><notAllowed>[]</notAllowed></allowed>
+		// after:  <allowed><notAllowed></notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
+		if ( splitResult.cursorParent ) {
+			data.cursorPosition = ModelPosition.createAt( splitResult.cursorParent );
+
+			// Otherwise just continue after inserted element.
+		} else {
+			data.cursorPosition = data.modelRange.end;
+		}
+	};
+}
+
+// Helper function for upcasting-to-element converter. Takes the model configuration, the converted view element
+// and a writer instance and returns a model element instance to be inserted in the model.
+//
+// @param {String|Function|module:engine/model/element~Element} model Model conversion configuration.
+// @param {module:engine/view/node~Node} input The converted view node.
+// @param {module:engine/model/writer~Writer} writer A writer instance to use to create the model element.
+function _getModelElement( model, input, writer ) {
+	if ( model instanceof Function ) {
+		return model( input, writer );
+	} else if ( typeof model == 'string' ) {
+		return writer.createElement( model );
+	} else {
+		return model;
+	}
+}
+
+// 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 = 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 );
+
+	const key = typeof config.model != 'object' ? config.model : config.model.key;
+	const value = typeof config.model != 'object' ? defaultModelValue : config.model.value;
+
+	config.model = { key, value };
+}
+
+// Helper for to-model-attribute conversion. Takes the model attribute name and conversion configuration and returns
+// a proper converter function.
+//
+// @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.
+// @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 ) => {
+		const match = matcher.match( data.viewItem );
+
+		// If there is no match, this callback should not do anything.
+		if ( !match ) {
+			return;
+		}
+
+		const modelKey = config.model.key;
+		const modelValue = typeof config.model.value == 'function' ? config.model.value( data.viewItem ) : config.model.value;
+
+		// Do not convert if attribute building function returned falsy value.
+		if ( modelValue === null ) {
+			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;
+		}
+
+		// Since we are converting to attribute we need an range on which we will set the attribute.
+		// If the range is not created yet, we will create it.
+		if ( !data.modelRange ) {
+			// Convert children and set conversion result as a current data.
+			data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.cursorPosition ) );
+		}
+
+		// Set attribute on current `output`. `Schema` is checked inside this helper function.
+		const attributeWasSet = _setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, conversionApi );
+
+		if ( attributeWasSet ) {
+			conversionApi.consumable.consume( data.viewItem, match.match );
+		}
+	};
+}
+
+// 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;
+
+	// Set attribute on each item in range according to Schema.
+	for ( const node of Array.from( modelRange.getItems() ) ) {
+		if ( conversionApi.schema.checkAttribute( node, modelAttribute.key ) ) {
+			conversionApi.writer.setAttribute( modelAttribute.key, modelAttribute.value, node );
+
+			result = true;
+		}
+	}
+
+	return result;
+}
+
+// Helper function for upcasting-to-marker conversion. Takes the config in a format requested by `upcastElementToMarker()`
+// function and converts it to a format that is supported by `upcastElementToElement()` function.
+//
+// @param {Object} config Conversion configuration.
+function _normalizeToMarkerConfig( config ) {
+	const oldModel = config.model;
+
+	config.model = ( viewElement, writer ) => {
+		const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement );
+
+		return writer.createElement( '$marker', { 'data-name': markerName } );
+	};
+}
+
+/**
  * Function factory, creates a converter that converts {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
  * or all children of {@link module:engine/view/element~Element} into
  * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
@@ -52,7 +560,7 @@ export function convertText() {
 
 				conversionApi.writer.insert( text, data.modelCursor );
 
-				data.modelRange = Range.createFromPositionAndShift( data.modelCursor, text.offsetSize );
+				data.modelRange = ModelRange.createFromPositionAndShift( data.modelCursor, text.offsetSize );
 				data.modelCursor = data.modelRange.end;
 			}
 		}

+ 0 - 518
packages/ckeditor5-engine/src/conversion/upcast-helpers.js

@@ -1,518 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Matcher from '../view/matcher';
-
-import ModelRange from '../model/range';
-import ModelPosition from '../model/position';
-
-import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
-
-/**
- * @module engine/conversion/upcast-helpers
- */
-
-/**
- * View element to model element conversion helper.
- *
- * This conversion results in creating a model element. For example, view `<p>Foo</p>` becomes `<paragraph>Foo</paragraph>` in the model.
- *
- * Keep in mind that the element will be inserted only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
- *
- *		upcastElementToElement( { view: 'p', model: 'paragraph' } );
- *
- *		upcastElementToElement( { view: 'p', model: 'paragraph' }, 'high' );
- *
- *		upcastElementToElement( {
- *			view: {
- *				name: 'p',
- *				class: 'fancy'
- *			},
- *			model: 'fancyParagraph'
- *		} );
- *
- *		upcastElementToElement( {
- *			view: {
- *				name: 'p',
- *				class: 'fancy'
- *			},
- *			model: new ModelElement( 'p', { fancy: true } )
- *		} );
- *
- *		upcastElementToElement( {
- * 			view: {
- *				name: 'p',
- *				class: 'heading'
- * 			},
- * 			model: viewElement => new ModelElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } )
- * 		} );
- *
- * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
- *
- * @param {Object} config Conversion configuration.
- * @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.
- * @returns {Function} Conversion helper.
- */
-export function upcastElementToElement( config, priority = 'normal' ) {
-	config = cloneDeep( config );
-
-	const converter = _prepareToElementConverter( config );
-
-	const elementName = _getViewElementNameFromConfig( config );
-	const eventName = elementName ? 'element:' + elementName : 'element';
-
-	return dispatcher => {
-		dispatcher.on( eventName, converter, { priority } );
-	};
-}
-
-/**
- * View element to model attribute conversion helper.
- *
- * This conversion results in setting an attribute on a model node. For example, view `<strong>Foo</strong>` becomes
- * `Foo` {@link module:engine/model/text~Text model text node} with `bold` attribute set to `true`.
- *
- * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
- *
- *		upcastElementToAttribute( { view: 'strong', model: 'bold' } );
- *
- *		upcastElementToAttribute( { view: 'strong', model: 'bold' }, 'normal' );
- *
- *		upcastElementToAttribute( {
- *			view: {
- *				name: 'span',
- *				class: 'bold'
- *			},
- *			model: 'bold'
- *		} );
- *
- *		upcastElementToAttribute( {
- *			view: {
- *				name: 'span',
- *				class: [ 'styled', 'styled-dark' ]
- *			},
- *			model: {
- *				key: 'styled',
- *				value: 'dark'
- *			}
- *		} );
- *
- * 		upcastElementToAttribute( {
- *			view: {
- *				name: 'span',
- *				style: {
- *					'font-size': /[\s\S]+/
- *				}
- *			},
- *			model: {
- *				key: 'fontSize',
- *				value: viewElement => {
- *					const fontSize = viewElement.getStyle( 'font-size' );
- *					const value = fontSize.substr( 0, fontSize.length - 2 );
- *
- *					if ( value <= 10 ) {
- *						return 'small';
- *					} else if ( value > 12 ) {
- *						return 'big';
- *					}
- *
- *					return null;
- *				}
- *			}
- *		} );
- *
- * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
- *
- * @param {Object} config Conversion configuration.
- * @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`.
- * @param {module:utils/priorities~PriorityString} [priority='low'] Converter priority.
- * @returns {Function} Conversion helper.
- */
-export function upcastElementToAttribute( config, priority = 'low' ) {
-	config = cloneDeep( config );
-
-	_normalizeModelAttributeConfig( config );
-
-	const converter = _prepareToAttributeConverter( config, true );
-
-	const elementName = _getViewElementNameFromConfig( config );
-	const eventName = elementName ? 'element:' + elementName : 'element';
-
-	return dispatcher => {
-		dispatcher.on( eventName, converter, { priority } );
-	};
-}
-
-/**
- * View attribute to model attribute conversion helper.
- *
- * This conversion results in setting an attribute on a model node. For example, view `<img src="foo.jpg"></img>` becomes
- * `<image source="foo.jpg"></image>` in the model.
- *
- * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
- *
- *		upcastAttributeToAttribute( { view: 'src', model: 'source' } );
- *
- *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } );
- *
- *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
- *
- *		upcastAttributeToAttribute( {
- *			view: {
- *				key: 'data-style',
- *				value: /[\s\S]+/
- *			},
- *			model: 'styled'
- *		} );
- *
- *		upcastAttributeToAttribute( {
- *			view: {
- *				name: 'span',
- *				key: 'class',
- *				value: 'styled-dark'
- *			},
- *			model: {
- *				key: 'styled',
- *				value: 'dark'
- *			}
- *		} );
- *
- *		upcastAttributeToAttribute( {
- *			view: {
- *				key: 'class',
- *				value: /styled-[\S]+/
- *			},
- *			model: {
- *				key: 'styled'
- *				value: viewElement => {
- *					const regexp = /styled-([\S]+)/;
- *					const match = viewElement.getAttribute( 'class' ).match( regexp );
- *
- *					return match[ 1 ];
- *				}
- *			}
- *		} );
- *
- * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
- *
- * @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 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.
- * @param {module:utils/priorities~PriorityString} [priority='low'] Converter priority.
- * @returns {Function} Conversion helper.
- */
-export function upcastAttributeToAttribute( config, priority = 'low' ) {
-	config = cloneDeep( config );
-
-	let viewKey = null;
-
-	if ( typeof config.view == 'string' || config.view.key ) {
-		viewKey = _normalizeViewAttributeKeyValueConfig( config );
-	}
-
-	_normalizeModelAttributeConfig( config, viewKey );
-
-	const converter = _prepareToAttributeConverter( config, false );
-
-	return dispatcher => {
-		dispatcher.on( 'element', converter, { priority } );
-	};
-}
-
-/**
- * View element to model marker conversion helper.
- *
- * This conversion results in creating a model marker. For example, if the marker was stored in a view as an element:
- * `<p>Fo<span data-marker="comment" data-comment-id="7"></span>o</p><p>B<span data-marker="comment" data-comment-id="7"></span>ar</p>`,
- * after the conversion is done, the marker will be available in
- * {@link module:engine/model/model~Model#markers model document markers}.
- *
- *		upcastElementToMarker( { view: 'marker-search', model: 'search' } );
- *
- *		upcastElementToMarker( { view: 'marker-search', model: 'search' }, 'high' );
- *
- *		upcastElementToMarker( {
- *			view: 'marker-search',
- *			model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
- *		} );
- *
- *		upcastElementToMarker( {
- *			view: {
- *				name: 'span',
- *				attribute: {
- *					'data-marker': 'search'
- *				}
- *			},
- *			model: 'search'
- *		} );
- *
- * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
- *
- * @param {Object} config Conversion configuration.
- * @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.
- * @returns {Function} Conversion helper.
- */
-export function upcastElementToMarker( config, priority = 'normal' ) {
-	config = cloneDeep( config );
-
-	_normalizeToMarkerConfig( config );
-
-	return upcastElementToElement( 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.
-// @returns {Function} View to model converter.
-function _prepareToElementConverter( config ) {
-	const matcher = new Matcher( config.view );
-
-	return ( evt, data, conversionApi ) => {
-		// This will be usually just one pattern but we support matchers with many patterns too.
-		const match = matcher.match( data.viewItem );
-
-		// If there is no match, this callback should not do anything.
-		if ( !match ) {
-			return;
-		}
-
-		// Create model element basing on config.
-		const modelElement = _getModelElement( config.model, data.viewItem, conversionApi.writer );
-
-		// Do not convert if element building function returned falsy value.
-		if ( !modelElement ) {
-			return;
-		}
-
-		// When element was already consumed then skip it.
-		if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) {
-			return;
-		}
-
-		// Find allowed parent for element that we are going to insert.
-		// If current parent does not allow to insert element but one of the ancestors does
-		// then split nodes to allowed parent.
-		const splitResult = conversionApi.splitToAllowedParent( modelElement, data.cursorPosition );
-
-		// When there is no split result it means that we can't insert element to model tree, so let's skip it.
-		if ( !splitResult ) {
-			return;
-		}
-
-		// Insert element on allowed position.
-		conversionApi.writer.insert( modelElement, splitResult.position );
-
-		// Convert children and insert to element.
-		const childrenResult = conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( modelElement ) );
-
-		// Consume appropriate value from consumable values list.
-		conversionApi.consumable.consume( data.viewItem, match.match );
-
-		// Set conversion result range.
-		data.modelRange = new ModelRange(
-			// Range should start before inserted element
-			ModelPosition.createBefore( modelElement ),
-			// Should end after but we need to take into consideration that children could split our
-			// element, so we need to move range after parent of the last converted child.
-			// before: <allowed>[]</allowed>
-			// after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
-			ModelPosition.createAfter( childrenResult.cursorPosition.parent )
-		);
-
-		// Now we need to check where the cursorPosition should be.
-		// If we had to split parent to insert our element then we want to continue conversion inside split parent.
-		//
-		// before: <allowed><notAllowed>[]</notAllowed></allowed>
-		// after:  <allowed><notAllowed></notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
-		if ( splitResult.cursorParent ) {
-			data.cursorPosition = ModelPosition.createAt( splitResult.cursorParent );
-
-			// Otherwise just continue after inserted element.
-		} else {
-			data.cursorPosition = data.modelRange.end;
-		}
-	};
-}
-
-// Helper function for upcasting-to-element converter. Takes the model configuration, the converted view element
-// and a writer instance and returns a model element instance to be inserted in the model.
-//
-// @param {String|Function|module:engine/model/element~Element} model Model conversion configuration.
-// @param {module:engine/view/node~Node} input The converted view node.
-// @param {module:engine/model/writer~Writer} writer A writer instance to use to create the model element.
-function _getModelElement( model, input, writer ) {
-	if ( model instanceof Function ) {
-		return model( input, writer );
-	} else if ( typeof model == 'string' ) {
-		return writer.createElement( model );
-	} else {
-		return model;
-	}
-}
-
-// 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 = 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 );
-
-	const key = typeof config.model != 'object' ? config.model : config.model.key;
-	const value = typeof config.model != 'object' ? defaultModelValue : config.model.value;
-
-	config.model = { key, value };
-}
-
-// Helper for to-model-attribute conversion. Takes the model attribute name and conversion configuration and returns
-// a proper converter function.
-//
-// @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.
-// @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 ) => {
-		const match = matcher.match( data.viewItem );
-
-		// If there is no match, this callback should not do anything.
-		if ( !match ) {
-			return;
-		}
-
-		const modelKey = config.model.key;
-		const modelValue = typeof config.model.value == 'function' ? config.model.value( data.viewItem ) : config.model.value;
-
-		// Do not convert if attribute building function returned falsy value.
-		if ( modelValue === null ) {
-			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;
-		}
-
-		// Since we are converting to attribute we need an range on which we will set the attribute.
-		// If the range is not created yet, we will create it.
-		if ( !data.modelRange ) {
-			// Convert children and set conversion result as a current data.
-			data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.cursorPosition ) );
-		}
-
-		// Set attribute on current `output`. `Schema` is checked inside this helper function.
-		const attributeWasSet = _setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, conversionApi );
-
-		if ( attributeWasSet ) {
-			conversionApi.consumable.consume( data.viewItem, match.match );
-		}
-	};
-}
-
-// 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;
-
-	// Set attribute on each item in range according to Schema.
-	for ( const node of Array.from( modelRange.getItems() ) ) {
-		if ( conversionApi.schema.checkAttribute( node, modelAttribute.key ) ) {
-			conversionApi.writer.setAttribute( modelAttribute.key, modelAttribute.value, node );
-
-			result = true;
-		}
-	}
-
-	return result;
-}
-
-// Helper function for upcasting-to-marker conversion. Takes the config in a format requested by `upcastElementToMarker()`
-// function and converts it to a format that is supported by `upcastElementToElement()` function.
-//
-// @param {Object} config Conversion configuration.
-function _normalizeToMarkerConfig( config ) {
-	const oldModel = config.model;
-
-	config.model = ( viewElement, writer ) => {
-		const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement );
-
-		return writer.createElement( '$marker', { 'data-name': markerName } );
-	};
-}

+ 2 - 2
packages/ckeditor5-engine/src/model/markercollection.js

@@ -274,9 +274,9 @@ mix( MarkerCollection, EmitterMixin );
  *
  * Markers downcast happens on {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} and
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} events.
- * Use {@link module:engine/conversion/downcast-helpers downcast helpers} or attach a custom converter to mentioned events.
+ * Use {@link module:engine/conversion/downcast-converters downcast converters} or attach a custom converter to mentioned events.
  * For {@link module:engine/controller/datacontroller~DataController data pipeline}, marker should be downcasted to an element.
- * Then, it can be upcasted back to a marker. Again, use {@link module:engine/conversion/upcast-helpers upcast helpers} or
+ * Then, it can be upcasted back to a marker. Again, use {@link module:engine/conversion/upcast-converters upcast converters} or
  * attach a custom converter to {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element}.
  *
  * Another upside of markers is that finding marked part of document is fast and easy. Using attributes to mark some nodes

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

@@ -19,13 +19,13 @@ import count from '@ckeditor/ckeditor5-utils/src/count';
 import {
 	upcastElementToElement,
 	upcastElementToAttribute
-} from '../../src/conversion/upcast-helpers';
+} from '../../src/conversion/upcast-converters';
 
 import {
 	downcastElementToElement,
 	downcastAttributeToElement,
 	downcastMarkerToHighlight
-} from '../../src/conversion/downcast-helpers';
+} from '../../src/conversion/downcast-converters';
 
 describe( 'DataController', () => {
 	let model, modelDocument, htmlDataProcessor, data, schema;

+ 1 - 1
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -14,7 +14,7 @@ import ViewDocument from '../../src/view/document';
 import Mapper from '../../src/conversion/mapper';
 import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
 
-import { downcastElementToElement, downcastMarkerToHighlight } from '../../src/conversion/downcast-helpers';
+import { downcastElementToElement, downcastMarkerToHighlight } from '../../src/conversion/downcast-converters';
 
 import Model from '../../src/model/model';
 import ModelPosition from '../../src/model/position';

+ 514 - 10
packages/ckeditor5-engine/tests/conversion/downcast-converters.js

@@ -3,6 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
+import EditingController from '../../src/controller/editingcontroller';
+
+import Conversion from '../../src/conversion/conversion';
+
 import Model from '../../src/model/model';
 import ModelElement from '../../src/model/element';
 import ModelText from '../../src/model/text';
@@ -16,18 +20,518 @@ import ViewUIElement from '../../src/view/uielement';
 import ViewText from '../../src/view/text';
 
 import {
-	insertElement,
-	insertUIElement,
-	changeAttribute,
-	wrap,
-	removeUIElement,
-	highlightElement,
-	highlightText,
-	removeHighlight,
-	createViewElementFromHighlightDescriptor
+	downcastElementToElement, downcastAttributeToElement, downcastAttributeToAttribute, downcastMarkerToElement, downcastMarkerToHighlight,
+	insertElement, insertUIElement, changeAttribute, wrap, removeUIElement,
+	highlightElement, highlightText, removeHighlight, createViewElementFromHighlightDescriptor
 } from '../../src/conversion/downcast-converters';
 
-import EditingController from '../../src/controller/editingcontroller';
+import { stringify } from '../../src/dev-utils/view';
+
+describe( 'downcast-helpers', () => {
+	let conversion, model, modelRoot, viewRoot;
+
+	beforeEach( () => {
+		model = new Model();
+		const modelDoc = model.document;
+		modelRoot = modelDoc.createRoot();
+
+		const controller = new EditingController( model );
+
+		// Set name of view root the same as dom root.
+		// This is a mock of attaching view root to dom root.
+		controller.view.getRoot()._name = 'div';
+
+		viewRoot = controller.view.getRoot();
+
+		conversion = new Conversion();
+		conversion.register( 'downcast', [ controller.downcastDispatcher ] );
+	} );
+
+	describe( 'downcastElementToElement', () => {
+		it( 'config.view is a string', () => {
+			const helper = downcastElementToElement( { model: 'paragraph', view: 'p' } );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'paragraph', modelRoot, 0 );
+			} );
+
+			expectResult( '<p></p>' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = downcastElementToElement( { model: 'paragraph', view: 'p' } );
+			const helperB = downcastElementToElement( { model: 'paragraph', view: 'foo' }, 'high' );
+
+			conversion.for( 'downcast' ).add( helperA ).add( helperB );
+
+			model.change( writer => {
+				writer.insertElement( 'paragraph', modelRoot, 0 );
+			} );
+
+			expectResult( '<foo></foo>' );
+		} );
+
+		it( 'config.view is an element instance', () => {
+			const helper = downcastElementToElement( {
+				model: 'paragraph',
+				view: new ViewContainerElement( 'p' )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'paragraph', modelRoot, 0 );
+			} );
+
+			expectResult( '<p></p>' );
+		} );
+
+		it( 'config.view is a view element definition', () => {
+			const helper = downcastElementToElement( {
+				model: 'fancyParagraph',
+				view: {
+					name: 'p',
+					class: 'fancy'
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'fancyParagraph', modelRoot, 0 );
+			} );
+
+			expectResult( '<p class="fancy"></p>' );
+		} );
+
+		it( 'config.view is a function', () => {
+			const helper = downcastElementToElement( {
+				model: 'heading',
+				view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'heading', { level: 2 }, modelRoot, 0 );
+			} );
+
+			expectResult( '<h2></h2>' );
+		} );
+	} );
+
+	describe( 'downcastAttributeToElement', () => {
+		it( 'config.view is a string', () => {
+			const helper = downcastAttributeToElement( 'bold', { view: 'strong' } );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
+			} );
+
+			expectResult( '<strong>foo</strong>' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = downcastAttributeToElement( 'bold', { view: 'strong' } );
+			const helperB = downcastAttributeToElement( 'bold', { view: 'b' }, 'high' );
+
+			conversion.for( 'downcast' ).add( helperA ).add( helperB );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
+			} );
+
+			expectResult( '<b>foo</b>' );
+		} );
+
+		it( 'config.view is an element instance', () => {
+			const helper = downcastAttributeToElement( 'bold', {
+				view: new ViewAttributeElement( 'strong' )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
+			} );
+
+			expectResult( '<strong>foo</strong>' );
+		} );
+
+		it( 'config.view is a view element definition', () => {
+			const helper = downcastAttributeToElement( 'bold', {
+				view: {
+					name: 'span',
+					class: 'bold'
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
+			} );
+
+			expectResult( '<span class="bold">foo</span>' );
+		} );
+
+		it( 'config.view is a view element definition, model attribute value specified', () => {
+			const helper = downcastAttributeToElement( 'styled', {
+				model: 'dark',
+				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: {
+						name: 'span',
+						style: {
+							'font-size': '1.2em'
+						}
+					}
+				},
+				{
+					model: 'small',
+					view: {
+						name: 'span',
+						style: {
+							'font-size': '0.8em'
+						}
+					}
+				}
+			] );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { fontSize: 'big' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<span style="font-size:1.2em">foo</span>' );
+
+			model.change( writer => {
+				writer.setAttribute( 'fontSize', 'small', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( '<span style="font-size:0.8em">foo</span>' );
+
+			model.change( writer => {
+				writer.removeAttribute( 'fontSize', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( 'foo' );
+		} );
+
+		it( 'config.view is a function', () => {
+			const helper = downcastAttributeToElement( 'bold', {
+				view: attributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + attributeValue } )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { bold: '500' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<span style="font-weight:500">foo</span>' );
+		} );
+	} );
+
+	describe( 'downcastAttributeToAttribute', () => {
+		beforeEach( () => {
+			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'image', view: 'img' } ) );
+		} );
+
+		it( 'config not set', () => {
+			const helper = downcastAttributeToAttribute( 'src' );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { src: '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 );
+			} );
+
+			expectResult( '<img src="foo.jpg"></img>' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = downcastAttributeToAttribute( 'source', { view: 'href' } );
+			const helperB = downcastAttributeToAttribute( 'source', { view: 'src' }, 'high' );
+
+			conversion.for( 'downcast' ).add( helperA ).add( helperB );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img src="foo.jpg"></img>' );
+		} );
+
+		it( 'config.view is an object', () => {
+			const helper = downcastAttributeToAttribute( 'stylish', { view: { key: 'class', value: 'styled' } } );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { stylish: true }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img class="styled"></img>' );
+		} );
+
+		it( 'config.view is an object, model attribute value specified', () => {
+			const helper = downcastAttributeToAttribute( 'styled', {
+				model: 'dark',
+				view: {
+					key: 'class',
+					value: 'styled-dark styled'
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img class="styled styled-dark"></img>' );
+
+			model.change( writer => {
+				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( '<img></img>' );
+		} );
+
+		it( 'multiple config items', () => {
+			const helper = downcastAttributeToAttribute( 'styled', [
+				{
+					model: 'dark',
+					view: {
+						key: 'class',
+						value: 'styled-dark'
+					}
+				},
+				{
+					model: 'light',
+					view: {
+						key: 'class',
+						value: 'styled-light'
+					}
+				}
+			] );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img class="styled-dark"></img>' );
+
+			model.change( writer => {
+				writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( '<img class="styled-light"></img>' );
+
+			model.change( writer => {
+				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( '<img></img>' );
+		} );
+
+		it( 'config.view is a function', () => {
+			const helper = downcastAttributeToAttribute( 'styled', {
+				view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { styled: 'pull-out' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img class="styled-pull-out"></img>' );
+		} );
+	} );
+
+	describe( 'downcastMarkerToElement', () => {
+		it( 'config.view is a string', () => {
+			const helper = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+			} );
+
+			expectResult( 'f<marker-search></marker-search>o<marker-search></marker-search>o' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
+			const helperB = downcastMarkerToElement( { model: 'search', view: 'search' }, 'high' );
+
+			conversion.for( 'downcast' ).add( helperA ).add( helperB );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+			} );
+
+			expectResult( 'f<search></search>o<search></search>o' );
+		} );
+
+		it( 'config.view is an element instance', () => {
+			const helper = downcastMarkerToElement( {
+				model: 'search',
+				view: new ViewUIElement( 'span', { 'data-marker': 'search' } )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+			} );
+
+			expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
+		} );
+
+		it( 'config.view is a view element definition', () => {
+			const helper = downcastMarkerToElement( {
+				model: 'search',
+				view: {
+					name: 'span',
+					attribute: {
+						'data-marker': 'search'
+					}
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+			} );
+
+			expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
+		} );
+
+		it( 'config.view is a function', () => {
+			const helper = downcastMarkerToElement( {
+				model: 'search',
+				view: data => {
+					return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+			} );
+
+			expectResult( 'f<span data-marker="search" data-start="true"></span>o<span data-marker="search" data-start="false"></span>o' );
+		} );
+	} );
+
+	describe( 'downcastMarkerToHighlight', () => {
+		it( 'config.view is a highlight descriptor', () => {
+			const helper = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'comment', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
+			} );
+
+			expectResult( '<span class="comment">foo</span>' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
+			const helperB = downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
+
+			conversion.for( 'downcast' ).add( helperA ).add( helperB );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'comment', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
+			} );
+
+			expectResult( '<span class="new-comment">foo</span>' );
+		} );
+
+		it( 'config.view is a function', () => {
+			const helper = downcastMarkerToHighlight( {
+				model: 'comment',
+				view: data => {
+					const commentType = data.markerName.split( ':' )[ 1 ];
+
+					return {
+						class: [ 'comment', 'comment-' + commentType ]
+					};
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'comment:abc', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
+			} );
+
+			expectResult( '<span class="comment comment-abc">foo</span>' );
+		} );
+	} );
+
+	function expectResult( string ) {
+		expect( stringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string );
+	}
+} );
 
 describe( 'downcast-converters', () => {
 	let dispatcher, modelDoc, modelRoot, viewRoot, controller, modelRootStart, model;

+ 0 - 526
packages/ckeditor5-engine/tests/conversion/downcast-helpers.js

@@ -1,526 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import {
-	downcastElementToElement, downcastAttributeToElement, downcastAttributeToAttribute, downcastMarkerToElement, downcastMarkerToHighlight
-} from '../../src/conversion/downcast-helpers';
-
-import Conversion from '../../src/conversion/conversion';
-import EditingController from '../../src/controller/editingcontroller';
-
-import Model from '../../src/model/model';
-import ModelRange from '../../src/model/range';
-
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewAttributeElement from '../../src/view/attributeelement';
-import ViewUIElement from '../../src/view/uielement';
-
-import { stringify } from '../../src/dev-utils/view';
-
-describe( 'downcast-helpers', () => {
-	let conversion, model, modelRoot, viewRoot;
-
-	beforeEach( () => {
-		model = new Model();
-		const modelDoc = model.document;
-		modelRoot = modelDoc.createRoot();
-
-		const controller = new EditingController( model );
-
-		// Set name of view root the same as dom root.
-		// This is a mock of attaching view root to dom root.
-		controller.view.getRoot()._name = 'div';
-
-		viewRoot = controller.view.getRoot();
-
-		conversion = new Conversion();
-		conversion.register( 'downcast', [ controller.downcastDispatcher ] );
-	} );
-
-	describe( 'downcastElementToElement', () => {
-		it( 'config.view is a string', () => {
-			const helper = downcastElementToElement( { model: 'paragraph', view: 'p' } );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertElement( 'paragraph', modelRoot, 0 );
-			} );
-
-			expectResult( '<p></p>' );
-		} );
-
-		it( 'can be overwritten using priority', () => {
-			const helperA = downcastElementToElement( { model: 'paragraph', view: 'p' } );
-			const helperB = downcastElementToElement( { model: 'paragraph', view: 'foo' }, 'high' );
-
-			conversion.for( 'downcast' ).add( helperA ).add( helperB );
-
-			model.change( writer => {
-				writer.insertElement( 'paragraph', modelRoot, 0 );
-			} );
-
-			expectResult( '<foo></foo>' );
-		} );
-
-		it( 'config.view is an element instance', () => {
-			const helper = downcastElementToElement( {
-				model: 'paragraph',
-				view: new ViewContainerElement( 'p' )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertElement( 'paragraph', modelRoot, 0 );
-			} );
-
-			expectResult( '<p></p>' );
-		} );
-
-		it( 'config.view is a view element definition', () => {
-			const helper = downcastElementToElement( {
-				model: 'fancyParagraph',
-				view: {
-					name: 'p',
-					class: 'fancy'
-				}
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertElement( 'fancyParagraph', modelRoot, 0 );
-			} );
-
-			expectResult( '<p class="fancy"></p>' );
-		} );
-
-		it( 'config.view is a function', () => {
-			const helper = downcastElementToElement( {
-				model: 'heading',
-				view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertElement( 'heading', { level: 2 }, modelRoot, 0 );
-			} );
-
-			expectResult( '<h2></h2>' );
-		} );
-	} );
-
-	describe( 'downcastAttributeToElement', () => {
-		it( 'config.view is a string', () => {
-			const helper = downcastAttributeToElement( 'bold', { view: 'strong' } );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
-			} );
-
-			expectResult( '<strong>foo</strong>' );
-		} );
-
-		it( 'can be overwritten using priority', () => {
-			const helperA = downcastAttributeToElement( 'bold', { view: 'strong' } );
-			const helperB = downcastAttributeToElement( 'bold', { view: 'b' }, 'high' );
-
-			conversion.for( 'downcast' ).add( helperA ).add( helperB );
-
-			model.change( writer => {
-				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
-			} );
-
-			expectResult( '<b>foo</b>' );
-		} );
-
-		it( 'config.view is an element instance', () => {
-			const helper = downcastAttributeToElement( 'bold', {
-				view: new ViewAttributeElement( 'strong' )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
-			} );
-
-			expectResult( '<strong>foo</strong>' );
-		} );
-
-		it( 'config.view is a view element definition', () => {
-			const helper = downcastAttributeToElement( 'bold', {
-				view: {
-					name: 'span',
-					class: 'bold'
-				}
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
-			} );
-
-			expectResult( '<span class="bold">foo</span>' );
-		} );
-
-		it( 'config.view is a view element definition, model attribute value specified', () => {
-			const helper = downcastAttributeToElement( 'styled', {
-				model: 'dark',
-				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: {
-						name: 'span',
-						style: {
-							'font-size': '1.2em'
-						}
-					}
-				},
-				{
-					model: 'small',
-					view: {
-						name: 'span',
-						style: {
-							'font-size': '0.8em'
-						}
-					}
-				}
-			] );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', { fontSize: 'big' }, modelRoot, 0 );
-			} );
-
-			expectResult( '<span style="font-size:1.2em">foo</span>' );
-
-			model.change( writer => {
-				writer.setAttribute( 'fontSize', 'small', modelRoot.getChild( 0 ) );
-			} );
-
-			expectResult( '<span style="font-size:0.8em">foo</span>' );
-
-			model.change( writer => {
-				writer.removeAttribute( 'fontSize', modelRoot.getChild( 0 ) );
-			} );
-
-			expectResult( 'foo' );
-		} );
-
-		it( 'config.view is a function', () => {
-			const helper = downcastAttributeToElement( 'bold', {
-				view: attributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + attributeValue } )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', { bold: '500' }, modelRoot, 0 );
-			} );
-
-			expectResult( '<span style="font-weight:500">foo</span>' );
-		} );
-	} );
-
-	describe( 'downcastAttributeToAttribute', () => {
-		beforeEach( () => {
-			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'image', view: 'img' } ) );
-		} );
-
-		it( 'config not set', () => {
-			const helper = downcastAttributeToAttribute( 'src' );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertElement( 'image', { src: '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 );
-			} );
-
-			expectResult( '<img src="foo.jpg"></img>' );
-		} );
-
-		it( 'can be overwritten using priority', () => {
-			const helperA = downcastAttributeToAttribute( 'source', { view: 'href' } );
-			const helperB = downcastAttributeToAttribute( 'source', { view: 'src' }, 'high' );
-
-			conversion.for( 'downcast' ).add( helperA ).add( helperB );
-
-			model.change( writer => {
-				writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
-			} );
-
-			expectResult( '<img src="foo.jpg"></img>' );
-		} );
-
-		it( 'config.view is an object', () => {
-			const helper = downcastAttributeToAttribute( 'stylish', { view: { key: 'class', value: 'styled' } } );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertElement( 'image', { stylish: true }, modelRoot, 0 );
-			} );
-
-			expectResult( '<img class="styled"></img>' );
-		} );
-
-		it( 'config.view is an object, model attribute value specified', () => {
-			const helper = downcastAttributeToAttribute( 'styled', {
-				model: 'dark',
-				view: {
-					key: 'class',
-					value: 'styled-dark styled'
-				}
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
-			} );
-
-			expectResult( '<img class="styled styled-dark"></img>' );
-
-			model.change( writer => {
-				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
-			} );
-
-			expectResult( '<img></img>' );
-		} );
-
-		it( 'multiple config items', () => {
-			const helper = downcastAttributeToAttribute( 'styled', [
-				{
-					model: 'dark',
-					view: {
-						key: 'class',
-						value: 'styled-dark'
-					}
-				},
-				{
-					model: 'light',
-					view: {
-						key: 'class',
-						value: 'styled-light'
-					}
-				}
-			] );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
-			} );
-
-			expectResult( '<img class="styled-dark"></img>' );
-
-			model.change( writer => {
-				writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
-			} );
-
-			expectResult( '<img class="styled-light"></img>' );
-
-			model.change( writer => {
-				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
-			} );
-
-			expectResult( '<img></img>' );
-		} );
-
-		it( 'config.view is a function', () => {
-			const helper = downcastAttributeToAttribute( 'styled', {
-				view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertElement( 'image', { styled: 'pull-out' }, modelRoot, 0 );
-			} );
-
-			expectResult( '<img class="styled-pull-out"></img>' );
-		} );
-	} );
-
-	describe( 'downcastMarkerToElement', () => {
-		it( 'config.view is a string', () => {
-			const helper = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', modelRoot, 0 );
-				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
-			} );
-
-			expectResult( 'f<marker-search></marker-search>o<marker-search></marker-search>o' );
-		} );
-
-		it( 'can be overwritten using priority', () => {
-			const helperA = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
-			const helperB = downcastMarkerToElement( { model: 'search', view: 'search' }, 'high' );
-
-			conversion.for( 'downcast' ).add( helperA ).add( helperB );
-
-			model.change( writer => {
-				writer.insertText( 'foo', modelRoot, 0 );
-				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
-			} );
-
-			expectResult( 'f<search></search>o<search></search>o' );
-		} );
-
-		it( 'config.view is an element instance', () => {
-			const helper = downcastMarkerToElement( {
-				model: 'search',
-				view: new ViewUIElement( 'span', { 'data-marker': 'search' } )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', modelRoot, 0 );
-				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
-			} );
-
-			expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
-		} );
-
-		it( 'config.view is a view element definition', () => {
-			const helper = downcastMarkerToElement( {
-				model: 'search',
-				view: {
-					name: 'span',
-					attribute: {
-						'data-marker': 'search'
-					}
-				}
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', modelRoot, 0 );
-				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
-			} );
-
-			expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
-		} );
-
-		it( 'config.view is a function', () => {
-			const helper = downcastMarkerToElement( {
-				model: 'search',
-				view: data => {
-					return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
-				}
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', modelRoot, 0 );
-				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
-			} );
-
-			expectResult( 'f<span data-marker="search" data-start="true"></span>o<span data-marker="search" data-start="false"></span>o' );
-		} );
-	} );
-
-	describe( 'downcastMarkerToHighlight', () => {
-		it( 'config.view is a highlight descriptor', () => {
-			const helper = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', modelRoot, 0 );
-				writer.setMarker( 'comment', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
-			} );
-
-			expectResult( '<span class="comment">foo</span>' );
-		} );
-
-		it( 'can be overwritten using priority', () => {
-			const helperA = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
-			const helperB = downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
-
-			conversion.for( 'downcast' ).add( helperA ).add( helperB );
-
-			model.change( writer => {
-				writer.insertText( 'foo', modelRoot, 0 );
-				writer.setMarker( 'comment', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
-			} );
-
-			expectResult( '<span class="new-comment">foo</span>' );
-		} );
-
-		it( 'config.view is a function', () => {
-			const helper = downcastMarkerToHighlight( {
-				model: 'comment',
-				view: data => {
-					const commentType = data.markerName.split( ':' )[ 1 ];
-
-					return {
-						class: [ 'comment', 'comment-' + commentType ]
-					};
-				}
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', modelRoot, 0 );
-				writer.setMarker( 'comment:abc', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
-			} );
-
-			expectResult( '<span class="comment comment-abc">foo</span>' );
-		} );
-	} );
-
-	function expectResult( string ) {
-		expect( stringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string );
-	}
-} );

+ 2 - 2
packages/ckeditor5-engine/tests/conversion/two-way-helpers.js

@@ -5,7 +5,7 @@
 
 import {
 	elementToElement, attributeToElement, attributeToAttribute
-} from '../../src/conversion/two-way-helpers';
+} from '../../src/conversion/two-way-converters';
 
 import Conversion from '../../src/conversion/conversion';
 import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
@@ -20,7 +20,7 @@ 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-helpers', () => {
+describe( 'two-way-converters', () => {
 	let viewDispatcher, model, schema, conversion, modelRoot, viewRoot;
 
 	beforeEach( () => {

+ 599 - 1
packages/ckeditor5-engine/tests/conversion/upcast-converters.js

@@ -3,10 +3,14 @@
  * For licensing, see LICENSE.md.
  */
 
+import Conversion from '../../src/conversion/conversion';
 import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
+
 import ViewContainerElement from '../../src/view/containerelement';
 import ViewDocumentFragment from '../../src/view/documentfragment';
 import ViewText from '../../src/view/text';
+import ViewUIElement from '../../src/view/uielement';
+import ViewAttributeElement from '../../src/view/attributeelement';
 
 import Model from '../../src/model/model';
 import ModelDocumentFragment from '../../src/model/documentfragment';
@@ -15,7 +19,601 @@ import ModelText from '../../src/model/text';
 import ModelRange from '../../src/model/range';
 import ModelPosition from '../../src/model/position';
 
-import { convertToModelFragment, convertText } from '../../src/conversion/upcast-converters';
+import {
+	upcastElementToElement, upcastElementToAttribute, upcastAttributeToAttribute, upcastElementToMarker,
+	convertToModelFragment, convertText
+} from '../../src/conversion/upcast-converters';
+
+import { stringify } from '../../src/dev-utils/model';
+
+describe( 'upcast-helpers', () => {
+	let dispatcher, model, schema, conversion;
+
+	beforeEach( () => {
+		model = new Model();
+
+		schema = model.schema;
+
+		schema.extend( '$text', {
+			allowIn: '$root'
+		} );
+
+		schema.register( '$marker', {
+			inheritAllFrom: '$block'
+		} );
+
+		schema.register( 'paragraph', {
+			inheritAllFrom: '$block'
+		} );
+
+		schema.extend( '$text', {
+			allowAttributes: [ 'bold' ]
+		} );
+
+		dispatcher = new UpcastDispatcher( model, { schema } );
+		dispatcher.on( 'text', convertText() );
+		dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+		dispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
+
+		conversion = new Conversion();
+		conversion.register( 'upcast', [ dispatcher ] );
+	} );
+
+	describe( 'upcastElementToElement', () => {
+		it( 'config.view is a string', () => {
+			const helper = upcastElementToElement( { view: 'p', model: 'paragraph' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			schema.register( 'p', {
+				inheritAllFrom: '$block'
+			} );
+
+			const helperA = upcastElementToElement( { view: 'p', model: 'p' } );
+			const helperB = upcastElementToElement( { view: 'p', model: 'paragraph' }, 'high' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
+		} );
+
+		it( 'config.view is an object', () => {
+			schema.register( 'fancyParagraph', {
+				inheritAllFrom: '$block'
+			} );
+
+			const helperParagraph = upcastElementToElement( { view: 'p', model: 'paragraph' } );
+			const helperFancy = upcastElementToElement( {
+				view: {
+					name: 'p',
+					class: 'fancy'
+				},
+				model: 'fancyParagraph'
+			}, 'high' );
+
+			conversion.for( 'upcast' ).add( helperParagraph ).add( helperFancy );
+
+			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
+			expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<fancyParagraph></fancyParagraph>' );
+		} );
+
+		it( 'config.model is element instance', () => {
+			schema.extend( 'paragraph', {
+				allowAttributes: [ 'fancy' ]
+			} );
+
+			const helper = upcastElementToElement( {
+				view: {
+					name: 'p',
+					class: 'fancy'
+				},
+				model: new ModelElement( 'paragraph', { fancy: true } )
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<paragraph fancy="true"></paragraph>' );
+		} );
+
+		it( 'config.model is a function', () => {
+			schema.register( 'heading', {
+				inheritAllFrom: '$block',
+				allowAttributes: [ 'level' ]
+			} );
+
+			const helper = upcastElementToElement( {
+				view: {
+					name: 'p',
+					class: 'heading'
+				},
+				model: viewElement => new ModelElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } )
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult( new ViewContainerElement( 'p', { class: 'heading', 'data-level': 2 } ), '<heading level="2"></heading>' );
+		} );
+
+		it( 'should fire conversion of the element children', () => {
+			const helper = upcastElementToElement( { view: 'p', model: 'paragraph' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), '<paragraph>foo</paragraph>' );
+		} );
+
+		it( 'should not insert a model element if it is not allowed by schema', () => {
+			const helper = upcastElementToElement( { view: 'h2', model: 'heading' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult( new ViewContainerElement( 'h2' ), '' );
+		} );
+
+		it( 'should auto-break elements', () => {
+			schema.register( 'heading', {
+				inheritAllFrom: '$block'
+			} );
+
+			const helperParagraph = upcastElementToElement( { view: 'p', model: 'paragraph' } );
+			const helperHeading = upcastElementToElement( { view: 'h2', model: 'heading' } );
+
+			conversion.for( 'upcast' ).add( helperParagraph ).add( helperHeading );
+
+			expectResult(
+				new ViewContainerElement( 'p', null, [
+					new ViewText( 'Foo' ),
+					new ViewContainerElement( 'h2', null, new ViewText( 'Xyz' ) ),
+					new ViewText( 'Bar' )
+				] ),
+				'<paragraph>Foo</paragraph><heading>Xyz</heading><paragraph>Bar</paragraph>'
+			);
+		} );
+
+		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' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
+		} );
+	} );
+
+	describe( 'upcastElementToAttribute', () => {
+		it( 'config.view is string', () => {
+			const helper = upcastElementToAttribute( { view: 'strong', model: 'bold' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
+				'<$text bold="true">foo</$text>'
+			);
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = upcastElementToAttribute( { view: 'strong', model: 'strong' } );
+			const helperB = upcastElementToAttribute( { view: 'strong', model: 'bold' }, 'high' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult(
+				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
+				'<$text bold="true">foo</$text>'
+			);
+		} );
+
+		it( 'config.view is an object', () => {
+			const helper = upcastElementToAttribute( {
+				view: {
+					name: 'span',
+					class: 'bold'
+				},
+				model: 'bold'
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'span', { class: 'bold' }, new ViewText( 'foo' ) ),
+				'<$text bold="true">foo</$text>'
+			);
+		} );
+
+		it( 'model attribute value is given', () => {
+			schema.extend( '$text', {
+				allowAttributes: [ 'styled' ]
+			} );
+
+			const helper = upcastElementToAttribute( {
+				view: {
+					name: 'span',
+					class: [ 'styled', 'styled-dark' ]
+				},
+				model: {
+					key: 'styled',
+					value: 'dark'
+				}
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'span', { class: 'styled styled-dark' }, new ViewText( 'foo' ) ),
+				'<$text styled="dark">foo</$text>'
+			);
+		} );
+
+		it( 'model attribute value is a function', () => {
+			schema.extend( '$text', {
+				allowAttributes: [ 'fontSize' ]
+			} );
+
+			const helper = upcastElementToAttribute( {
+				view: {
+					name: 'span',
+					style: {
+						'font-size': /[\s\S]+/
+					}
+				},
+				model: {
+					key: 'fontSize',
+					value: viewElement => {
+						const fontSize = viewElement.getStyle( 'font-size' );
+						const value = fontSize.substr( 0, fontSize.length - 2 );
+
+						if ( value <= 10 ) {
+							return 'small';
+						} else if ( value > 12 ) {
+							return 'big';
+						}
+
+						return null;
+					}
+				}
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'span', { style: 'font-size:9px' }, new ViewText( 'foo' ) ),
+				'<$text fontSize="small">foo</$text>'
+			);
+
+			expectResult(
+				new ViewAttributeElement( 'span', { style: 'font-size:12px' }, new ViewText( 'foo' ) ),
+				'foo'
+			);
+
+			expectResult(
+				new ViewAttributeElement( 'span', { style: 'font-size:14px' }, new ViewText( 'foo' ) ),
+				'<$text fontSize="big">foo</$text>'
+			);
+		} );
+
+		it( 'should not set an attribute if it is not allowed by schema', () => {
+			const helper = upcastElementToAttribute( { view: 'em', model: 'italic' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'em', null, new ViewText( 'foo' ) ),
+				'foo'
+			);
+		} );
+
+		it( 'should not do anything if returned model attribute is null', () => {
+			const helperA = upcastElementToAttribute( { view: 'strong', model: 'bold' } );
+			const helperB = upcastElementToAttribute( {
+				view: 'strong',
+				model: {
+					key: 'bold',
+					value: () => null
+				}
+			}, 'high' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult(
+				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
+				'<$text bold="true">foo</$text>'
+			);
+		} );
+	} );
+
+	describe( 'upcastAttributeToAttribute', () => {
+		beforeEach( () => {
+			conversion.for( 'upcast' ).add( upcastElementToElement( { view: 'img', model: 'image' } ) );
+
+			schema.register( 'image', {
+				inheritAllFrom: '$block'
+			} );
+		} );
+
+		it( 'config.view is a string', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'source' ]
+			} );
+
+			const helper = upcastAttributeToAttribute( { view: 'src', model: 'source' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
+				'<image source="foo.jpg"></image>'
+			);
+		} );
+
+		it( 'config.view has only key set', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'source' ]
+			} );
+
+			const helper = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } );
+
+			conversion.for( 'upcast' ).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 = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'src' } );
+			const helperB = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
+				'<image source="foo.jpg"></image>'
+			);
+		} );
+
+		it( 'config.view has value set', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'styled' ]
+			} );
+
+			const helper = upcastAttributeToAttribute( {
+				view: {
+					key: 'data-style',
+					value: /[\s\S]*/
+				},
+				model: 'styled'
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { 'data-style': 'dark' } ),
+				'<image styled="dark"></image>'
+			);
+		} );
+
+		it( 'model attribute value is a string', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'styled' ]
+			} );
+
+			const helper = upcastAttributeToAttribute( {
+				view: {
+					name: 'img',
+					key: 'class',
+					value: 'styled-dark'
+				},
+				model: {
+					key: 'styled',
+					value: 'dark'
+				}
+			} );
+
+			conversion.for( 'upcast' )
+				.add( helper )
+				.add( upcastElementToElement( { view: 'p', model: 'paragraph' } ) );
+
+			expectResult(
+				new ViewContainerElement( 'img', { class: 'styled-dark' } ),
+				'<image styled="dark"></image>'
+			);
+
+			expectResult(
+				new ViewContainerElement( 'img', { class: 'styled-xxx' } ),
+				'<image></image>'
+			);
+
+			expectResult(
+				new ViewContainerElement( 'p', { class: 'styled-dark' } ),
+				'<paragraph></paragraph>'
+			);
+		} );
+
+		it( 'model attribute value is a function', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'styled' ]
+			} );
+
+			const helper = upcastAttributeToAttribute( {
+				view: {
+					key: 'class',
+					value: /styled-[\S]+/
+				},
+				model: {
+					key: 'styled',
+					value: viewElement => {
+						const regexp = /styled-([\S]+)/;
+						const match = viewElement.getAttribute( 'class' ).match( regexp );
+
+						return match[ 1 ];
+					}
+				}
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { 'class': 'styled-dark' } ),
+				'<image styled="dark"></image>'
+			);
+		} );
+
+		it( 'should not set an attribute if it is not allowed by schema', () => {
+			const helper = upcastAttributeToAttribute( { view: 'src', model: 'source' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
+				'<image></image>'
+			);
+		} );
+
+		it( 'should not do anything if returned model attribute is null', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'styled' ]
+			} );
+
+			const helperA = upcastAttributeToAttribute( {
+				view: {
+					key: 'class',
+					value: 'styled'
+				},
+				model: {
+					key: 'styled',
+					value: true
+				}
+			} );
+
+			const helperB = upcastAttributeToAttribute( {
+				view: {
+					key: 'class',
+					value: 'styled'
+				},
+				model: {
+					key: 'styled',
+					value: () => null
+				}
+			} );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { class: 'styled' } ),
+				'<image styled="true"></image>'
+			);
+		} );
+	} );
+
+	describe( 'upcastElementToMarker', () => {
+		it( 'config.view is a string', () => {
+			const helper = upcastElementToMarker( { view: 'marker-search', model: 'search' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			const frag = new ViewDocumentFragment( [
+				new ViewText( 'fo' ),
+				new ViewUIElement( 'marker-search' ),
+				new ViewText( 'oba' ),
+				new ViewUIElement( 'marker-search' ),
+				new ViewText( 'r' )
+			] );
+
+			const marker = { name: 'search', start: [ 2 ], end: [ 5 ] };
+
+			expectResult( frag, 'foobar', marker );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = upcastElementToMarker( { view: 'marker-search', model: 'search-result' } );
+			const helperB = upcastElementToMarker( { view: 'marker-search', model: 'search' }, 'high' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			const frag = new ViewDocumentFragment( [
+				new ViewText( 'fo' ),
+				new ViewUIElement( 'marker-search' ),
+				new ViewText( 'oba' ),
+				new ViewUIElement( 'marker-search' ),
+				new ViewText( 'r' )
+			] );
+
+			const marker = { name: 'search', start: [ 2 ], end: [ 5 ] };
+
+			expectResult( frag, 'foobar', marker );
+		} );
+
+		it( 'config.view is an object', () => {
+			const helper = upcastElementToMarker( {
+				view: {
+					name: 'span',
+					'data-marker': 'search'
+				},
+				model: 'search'
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			const frag = new ViewDocumentFragment( [
+				new ViewText( 'f' ),
+				new ViewUIElement( 'span', { 'data-marker': 'search' } ),
+				new ViewText( 'oob' ),
+				new ViewUIElement( 'span', { 'data-marker': 'search' } ),
+				new ViewText( 'ar' )
+			] );
+
+			const marker = { name: 'search', start: [ 1 ], end: [ 4 ] };
+
+			expectResult( frag, 'foobar', marker );
+		} );
+
+		it( 'config.model is a function', () => {
+			const helper = upcastElementToMarker( {
+				view: 'comment',
+				model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			const frag = new ViewDocumentFragment( [
+				new ViewText( 'foo' ),
+				new ViewUIElement( 'comment', { 'data-comment-id': 4 } ),
+				new ViewText( 'b' ),
+				new ViewUIElement( 'comment', { 'data-comment-id': 4 } ),
+				new ViewText( 'ar' )
+			] );
+
+			const marker = { name: 'comment:4', start: [ 3 ], end: [ 4 ] };
+
+			expectResult( frag, 'foobar', marker );
+		} );
+	} );
+
+	function expectResult( viewToConvert, modelString, marker ) {
+		const model = dispatcher.convert( viewToConvert );
+
+		if ( marker ) {
+			expect( model.markers.has( marker.name ) ).to.be.true;
+
+			const convertedMarker = model.markers.get( marker.name );
+
+			expect( convertedMarker.start.path ).to.deep.equal( marker.start );
+			expect( convertedMarker.end.path ).to.deep.equal( marker.end );
+		}
+
+		expect( stringify( model ) ).to.equal( modelString );
+	}
+} );
 
 describe( 'upcast-converters', () => {
 	let dispatcher, schema, context, model;

+ 0 - 612
packages/ckeditor5-engine/tests/conversion/upcast-helpers.js

@@ -1,612 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import {
-	upcastElementToElement, upcastElementToAttribute, upcastAttributeToAttribute, upcastElementToMarker
-} from '../../src/conversion/upcast-helpers';
-
-import Model from '../../src/model/model';
-import Conversion from '../../src/conversion/conversion';
-import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
-
-import ModelElement from '../../src/model/element';
-
-import ViewDocumentFragment from '../../src/view/documentfragment';
-import ViewUIElement from '../../src/view/uielement';
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewAttributeElement from '../../src/view/attributeelement';
-import ViewText from '../../src/view/text';
-
-import { convertText, convertToModelFragment } from '../../src/conversion/upcast-converters';
-import { stringify } from '../../src/dev-utils/model';
-
-describe( 'upcast-helpers', () => {
-	let dispatcher, model, schema, conversion;
-
-	beforeEach( () => {
-		model = new Model();
-
-		schema = model.schema;
-
-		schema.extend( '$text', {
-			allowIn: '$root'
-		} );
-
-		schema.register( '$marker', {
-			inheritAllFrom: '$block'
-		} );
-
-		schema.register( 'paragraph', {
-			inheritAllFrom: '$block'
-		} );
-
-		schema.extend( '$text', {
-			allowAttributes: [ 'bold' ]
-		} );
-
-		dispatcher = new UpcastDispatcher( model, { schema } );
-		dispatcher.on( 'text', convertText() );
-		dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
-		dispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
-
-		conversion = new Conversion();
-		conversion.register( 'upcast', [ dispatcher ] );
-	} );
-
-	describe( 'upcastElementToElement', () => {
-		it( 'config.view is a string', () => {
-			const helper = upcastElementToElement( { view: 'p', model: 'paragraph' } );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
-		} );
-
-		it( 'can be overwritten using priority', () => {
-			schema.register( 'p', {
-				inheritAllFrom: '$block'
-			} );
-
-			const helperA = upcastElementToElement( { view: 'p', model: 'p' } );
-			const helperB = upcastElementToElement( { view: 'p', model: 'paragraph' }, 'high' );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
-
-			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
-		} );
-
-		it( 'config.view is an object', () => {
-			schema.register( 'fancyParagraph', {
-				inheritAllFrom: '$block'
-			} );
-
-			const helperParagraph = upcastElementToElement( { view: 'p', model: 'paragraph' } );
-			const helperFancy = upcastElementToElement( {
-				view: {
-					name: 'p',
-					class: 'fancy'
-				},
-				model: 'fancyParagraph'
-			}, 'high' );
-
-			conversion.for( 'upcast' ).add( helperParagraph ).add( helperFancy );
-
-			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
-			expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<fancyParagraph></fancyParagraph>' );
-		} );
-
-		it( 'config.model is element instance', () => {
-			schema.extend( 'paragraph', {
-				allowAttributes: [ 'fancy' ]
-			} );
-
-			const helper = upcastElementToElement( {
-				view: {
-					name: 'p',
-					class: 'fancy'
-				},
-				model: new ModelElement( 'paragraph', { fancy: true } )
-			} );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<paragraph fancy="true"></paragraph>' );
-		} );
-
-		it( 'config.model is a function', () => {
-			schema.register( 'heading', {
-				inheritAllFrom: '$block',
-				allowAttributes: [ 'level' ]
-			} );
-
-			const helper = upcastElementToElement( {
-				view: {
-					name: 'p',
-					class: 'heading'
-				},
-				model: viewElement => new ModelElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } )
-			} );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult( new ViewContainerElement( 'p', { class: 'heading', 'data-level': 2 } ), '<heading level="2"></heading>' );
-		} );
-
-		it( 'should fire conversion of the element children', () => {
-			const helper = upcastElementToElement( { view: 'p', model: 'paragraph' } );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), '<paragraph>foo</paragraph>' );
-		} );
-
-		it( 'should not insert a model element if it is not allowed by schema', () => {
-			const helper = upcastElementToElement( { view: 'h2', model: 'heading' } );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult( new ViewContainerElement( 'h2' ), '' );
-		} );
-
-		it( 'should auto-break elements', () => {
-			schema.register( 'heading', {
-				inheritAllFrom: '$block'
-			} );
-
-			const helperParagraph = upcastElementToElement( { view: 'p', model: 'paragraph' } );
-			const helperHeading = upcastElementToElement( { view: 'h2', model: 'heading' } );
-
-			conversion.for( 'upcast' ).add( helperParagraph ).add( helperHeading );
-
-			expectResult(
-				new ViewContainerElement( 'p', null, [
-					new ViewText( 'Foo' ),
-					new ViewContainerElement( 'h2', null, new ViewText( 'Xyz' ) ),
-					new ViewText( 'Bar' )
-				] ),
-				'<paragraph>Foo</paragraph><heading>Xyz</heading><paragraph>Bar</paragraph>'
-			);
-		} );
-
-		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' );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
-
-			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
-		} );
-	} );
-
-	describe( 'upcastElementToAttribute', () => {
-		it( 'config.view is string', () => {
-			const helper = upcastElementToAttribute( { view: 'strong', model: 'bold' } );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult(
-				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
-				'<$text bold="true">foo</$text>'
-			);
-		} );
-
-		it( 'can be overwritten using priority', () => {
-			const helperA = upcastElementToAttribute( { view: 'strong', model: 'strong' } );
-			const helperB = upcastElementToAttribute( { view: 'strong', model: 'bold' }, 'high' );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
-
-			expectResult(
-				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
-				'<$text bold="true">foo</$text>'
-			);
-		} );
-
-		it( 'config.view is an object', () => {
-			const helper = upcastElementToAttribute( {
-				view: {
-					name: 'span',
-					class: 'bold'
-				},
-				model: 'bold'
-			} );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult(
-				new ViewAttributeElement( 'span', { class: 'bold' }, new ViewText( 'foo' ) ),
-				'<$text bold="true">foo</$text>'
-			);
-		} );
-
-		it( 'model attribute value is given', () => {
-			schema.extend( '$text', {
-				allowAttributes: [ 'styled' ]
-			} );
-
-			const helper = upcastElementToAttribute( {
-				view: {
-					name: 'span',
-					class: [ 'styled', 'styled-dark' ]
-				},
-				model: {
-					key: 'styled',
-					value: 'dark'
-				}
-			} );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult(
-				new ViewAttributeElement( 'span', { class: 'styled styled-dark' }, new ViewText( 'foo' ) ),
-				'<$text styled="dark">foo</$text>'
-			);
-		} );
-
-		it( 'model attribute value is a function', () => {
-			schema.extend( '$text', {
-				allowAttributes: [ 'fontSize' ]
-			} );
-
-			const helper = upcastElementToAttribute( {
-				view: {
-					name: 'span',
-					style: {
-						'font-size': /[\s\S]+/
-					}
-				},
-				model: {
-					key: 'fontSize',
-					value: viewElement => {
-						const fontSize = viewElement.getStyle( 'font-size' );
-						const value = fontSize.substr( 0, fontSize.length - 2 );
-
-						if ( value <= 10 ) {
-							return 'small';
-						} else if ( value > 12 ) {
-							return 'big';
-						}
-
-						return null;
-					}
-				}
-			} );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult(
-				new ViewAttributeElement( 'span', { style: 'font-size:9px' }, new ViewText( 'foo' ) ),
-				'<$text fontSize="small">foo</$text>'
-			);
-
-			expectResult(
-				new ViewAttributeElement( 'span', { style: 'font-size:12px' }, new ViewText( 'foo' ) ),
-				'foo'
-			);
-
-			expectResult(
-				new ViewAttributeElement( 'span', { style: 'font-size:14px' }, new ViewText( 'foo' ) ),
-				'<$text fontSize="big">foo</$text>'
-			);
-		} );
-
-		it( 'should not set an attribute if it is not allowed by schema', () => {
-			const helper = upcastElementToAttribute( { view: 'em', model: 'italic' } );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult(
-				new ViewAttributeElement( 'em', null, new ViewText( 'foo' ) ),
-				'foo'
-			);
-		} );
-
-		it( 'should not do anything if returned model attribute is null', () => {
-			const helperA = upcastElementToAttribute( { view: 'strong', model: 'bold' } );
-			const helperB = upcastElementToAttribute( {
-				view: 'strong',
-				model: {
-					key: 'bold',
-					value: () => null
-				}
-			}, 'high' );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
-
-			expectResult(
-				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
-				'<$text bold="true">foo</$text>'
-			);
-		} );
-	} );
-
-	describe( 'upcastAttributeToAttribute', () => {
-		beforeEach( () => {
-			conversion.for( 'upcast' ).add( upcastElementToElement( { view: 'img', model: 'image' } ) );
-
-			schema.register( 'image', {
-				inheritAllFrom: '$block'
-			} );
-		} );
-
-		it( 'config.view is a string', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'source' ]
-			} );
-
-			const helper = upcastAttributeToAttribute( { view: 'src', model: 'source' } );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult(
-				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
-				'<image source="foo.jpg"></image>'
-			);
-		} );
-
-		it( 'config.view has only key set', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'source' ]
-			} );
-
-			const helper = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } );
-
-			conversion.for( 'upcast' ).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 = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'src' } );
-			const helperB = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
-
-			expectResult(
-				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
-				'<image source="foo.jpg"></image>'
-			);
-		} );
-
-		it( 'config.view has value set', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'styled' ]
-			} );
-
-			const helper = upcastAttributeToAttribute( {
-				view: {
-					key: 'data-style',
-					value: /[\s\S]*/
-				},
-				model: 'styled'
-			} );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult(
-				new ViewAttributeElement( 'img', { 'data-style': 'dark' } ),
-				'<image styled="dark"></image>'
-			);
-		} );
-
-		it( 'model attribute value is a string', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'styled' ]
-			} );
-
-			const helper = upcastAttributeToAttribute( {
-				view: {
-					name: 'img',
-					key: 'class',
-					value: 'styled-dark'
-				},
-				model: {
-					key: 'styled',
-					value: 'dark'
-				}
-			} );
-
-			conversion.for( 'upcast' )
-				.add( helper )
-				.add( upcastElementToElement( { view: 'p', model: 'paragraph' } ) );
-
-			expectResult(
-				new ViewContainerElement( 'img', { class: 'styled-dark' } ),
-				'<image styled="dark"></image>'
-			);
-
-			expectResult(
-				new ViewContainerElement( 'img', { class: 'styled-xxx' } ),
-				'<image></image>'
-			);
-
-			expectResult(
-				new ViewContainerElement( 'p', { class: 'styled-dark' } ),
-				'<paragraph></paragraph>'
-			);
-		} );
-
-		it( 'model attribute value is a function', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'styled' ]
-			} );
-
-			const helper = upcastAttributeToAttribute( {
-				view: {
-					key: 'class',
-					value: /styled-[\S]+/
-				},
-				model: {
-					key: 'styled',
-					value: viewElement => {
-						const regexp = /styled-([\S]+)/;
-						const match = viewElement.getAttribute( 'class' ).match( regexp );
-
-						return match[ 1 ];
-					}
-				}
-			} );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult(
-				new ViewAttributeElement( 'img', { 'class': 'styled-dark' } ),
-				'<image styled="dark"></image>'
-			);
-		} );
-
-		it( 'should not set an attribute if it is not allowed by schema', () => {
-			const helper = upcastAttributeToAttribute( { view: 'src', model: 'source' } );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult(
-				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
-				'<image></image>'
-			);
-		} );
-
-		it( 'should not do anything if returned model attribute is null', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'styled' ]
-			} );
-
-			const helperA = upcastAttributeToAttribute( {
-				view: {
-					key: 'class',
-					value: 'styled'
-				},
-				model: {
-					key: 'styled',
-					value: true
-				}
-			} );
-
-			const helperB = upcastAttributeToAttribute( {
-				view: {
-					key: 'class',
-					value: 'styled'
-				},
-				model: {
-					key: 'styled',
-					value: () => null
-				}
-			} );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
-
-			expectResult(
-				new ViewAttributeElement( 'img', { class: 'styled' } ),
-				'<image styled="true"></image>'
-			);
-		} );
-	} );
-
-	describe( 'upcastElementToMarker', () => {
-		it( 'config.view is a string', () => {
-			const helper = upcastElementToMarker( { view: 'marker-search', model: 'search' } );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			const frag = new ViewDocumentFragment( [
-				new ViewText( 'fo' ),
-				new ViewUIElement( 'marker-search' ),
-				new ViewText( 'oba' ),
-				new ViewUIElement( 'marker-search' ),
-				new ViewText( 'r' )
-			] );
-
-			const marker = { name: 'search', start: [ 2 ], end: [ 5 ] };
-
-			expectResult( frag, 'foobar', marker );
-		} );
-
-		it( 'can be overwritten using priority', () => {
-			const helperA = upcastElementToMarker( { view: 'marker-search', model: 'search-result' } );
-			const helperB = upcastElementToMarker( { view: 'marker-search', model: 'search' }, 'high' );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
-
-			const frag = new ViewDocumentFragment( [
-				new ViewText( 'fo' ),
-				new ViewUIElement( 'marker-search' ),
-				new ViewText( 'oba' ),
-				new ViewUIElement( 'marker-search' ),
-				new ViewText( 'r' )
-			] );
-
-			const marker = { name: 'search', start: [ 2 ], end: [ 5 ] };
-
-			expectResult( frag, 'foobar', marker );
-		} );
-
-		it( 'config.view is an object', () => {
-			const helper = upcastElementToMarker( {
-				view: {
-					name: 'span',
-					'data-marker': 'search'
-				},
-				model: 'search'
-			} );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			const frag = new ViewDocumentFragment( [
-				new ViewText( 'f' ),
-				new ViewUIElement( 'span', { 'data-marker': 'search' } ),
-				new ViewText( 'oob' ),
-				new ViewUIElement( 'span', { 'data-marker': 'search' } ),
-				new ViewText( 'ar' )
-			] );
-
-			const marker = { name: 'search', start: [ 1 ], end: [ 4 ] };
-
-			expectResult( frag, 'foobar', marker );
-		} );
-
-		it( 'config.model is a function', () => {
-			const helper = upcastElementToMarker( {
-				view: 'comment',
-				model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
-			} );
-
-			conversion.for( 'upcast' ).add( helper );
-
-			const frag = new ViewDocumentFragment( [
-				new ViewText( 'foo' ),
-				new ViewUIElement( 'comment', { 'data-comment-id': 4 } ),
-				new ViewText( 'b' ),
-				new ViewUIElement( 'comment', { 'data-comment-id': 4 } ),
-				new ViewText( 'ar' )
-			] );
-
-			const marker = { name: 'comment:4', start: [ 3 ], end: [ 4 ] };
-
-			expectResult( frag, 'foobar', marker );
-		} );
-	} );
-
-	function expectResult( viewToConvert, modelString, marker ) {
-		const model = dispatcher.convert( viewToConvert );
-
-		if ( marker ) {
-			expect( model.markers.has( marker.name ) ).to.be.true;
-
-			const convertedMarker = model.markers.get( marker.name );
-
-			expect( convertedMarker.start.path ).to.deep.equal( marker.start );
-			expect( convertedMarker.end.path ).to.deep.equal( marker.end );
-		}
-
-		expect( stringify( model ) ).to.equal( modelString );
-	}
-} );

+ 2 - 2
packages/ckeditor5-engine/tests/manual/highlight.js

@@ -11,12 +11,12 @@ import ViewText from '../../src/view/text';
 
 import {
 	upcastElementToElement,
-} from '../../src/conversion/upcast-helpers';
+} from '../../src/conversion/upcast-converters';
 
 import {
 	downcastElementToElement,
 	downcastMarkerToHighlight
-} from '../../src/conversion/downcast-helpers';
+} from '../../src/conversion/downcast-converters';
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';

+ 1 - 1
packages/ckeditor5-engine/tests/manual/markers.js

@@ -17,7 +17,7 @@ import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 
 import {
 	downcastMarkerToHighlight
-} from '../../src/conversion/downcast-helpers';
+} from '../../src/conversion/downcast-converters';
 
 import Position from '../../src/model/position';
 import Range from '../../src/model/range';

+ 2 - 2
packages/ckeditor5-engine/tests/manual/nestededitable.js

@@ -7,11 +7,11 @@
 
 import {
 	upcastElementToElement
-} from '../../src/conversion/upcast-helpers';
+} from '../../src/conversion/upcast-converters';
 
 import {
 	downcastElementToElement
-} from '../../src/conversion/downcast-helpers';
+} from '../../src/conversion/downcast-converters';
 
 import ViewEditableElement from '../../src/view/editableelement';
 import { getData } from '../../src/dev-utils/model';

+ 2 - 2
packages/ckeditor5-engine/tests/manual/tickets/475/1.js

@@ -13,11 +13,11 @@ import LivePosition from '../../../../src/model/liveposition';
 
 import {
 	upcastElementToAttribute
-} from '../../../../src/conversion/upcast-helpers';
+} from '../../../../src/conversion/upcast-converters';
 
 import {
 	downcastAttributeToElement,
-} from '../../../../src/conversion/downcast-helpers';
+} from '../../../../src/conversion/downcast-converters';
 
 import AttributeElement from '../../../../src/view/attributeelement';
 

+ 2 - 2
packages/ckeditor5-engine/tests/tickets/699.js

@@ -10,11 +10,11 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import {
 	upcastElementToElement
-} from '../../src/conversion/upcast-helpers';
+} from '../../src/conversion/upcast-converters';
 
 import {
 	downcastElementToElement
-} from '../../src/conversion/downcast-helpers';
+} from '../../src/conversion/downcast-converters';
 
 import { getData as getModelData } from '../../src/dev-utils/model';
 import { getData as getViewData } from '../../src/dev-utils/view';