Преглед изворни кода

Merge pull request #1613 from ckeditor/t/1556

Enhancement: Expose conversion utilities. Closes #1556.

BREAKING CHANGE: The conversion.register() method now accepts single options object as a parameter.
BREAKING CHANGE: The downcastElementToElement() helper was removed from public API. Use conversion.for( 'downcast' ).elementToElement() instead.
BREAKING CHANGE: The downcastAttributeToElement() helper was removed from public API. Use conversion.for( 'downcast' ).attributeToElement() instead.
BREAKING CHANGE: The downcastAttributeToAttribute() helper was removed from public API. Use conversion.for( 'downcast' ).attributeToAttribute() instead.
BREAKING CHANGE: The downcastMarkerToElement() helper was removed from public API. Use conversion.for( 'downcast' ).markerToElement() instead.
BREAKING CHANGE: The downcastMarkerToHighlight() helper was removed from public API. Use conversion.for( 'downcast' ).markerToHighlight() instead.
BREAKING CHANGE: The upcastElementToElement() helper was removed from public API. Use conversion.for( 'upcast' ).elementToElement() instead.
BREAKING CHANGE: The upcastElementToAttribute() helper was removed from public API. Use conversion.for( 'upcast' ).elementToAttribute() instead.
BREAKING CHANGE: The upcastAttributeToAttribute() helper was removed from public API. Use conversion.for( 'upcast' ).attributeToAttribute() instead.
BREAKING CHANGE: The upcastElementToMarker() helper was removed from public API. Use conversion.for( 'upcast' ).elementToMarker() instead.
BREAKING CHANGE: The insertUIElement() and removeUIElement() downcast converters were removed from public API. Use conversion.for( 'downcast' ).markerToElement() instead.
BREAKING CHANGE: The highlightText(), highlightElement() and removeHighlight() downcast converters were removed from public API. Use conversion.for( 'downcast' ).markerToHighlight() instead.
BREAKING CHANGE: The insertElement() downcast converter was removed from public API. Use conversion.for( 'downcast' ).elementToElement() instead.
BREAKING CHANGE: The changeAttribute() downcast converter was removed from public API. Use conversion.for( 'downcast' ).attributeToAttribute() instead.
Piotr Jasiun пре 7 година
родитељ
комит
c1cd5e8e20
23 измењених фајлова са 2441 додато и 1647 уклоњено
  1. 2 2
      packages/ckeditor5-engine/src/controller/datacontroller.js
  2. 1 1
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  3. 105 86
      packages/ckeditor5-engine/src/conversion/conversion.js
  4. 452 470
      packages/ckeditor5-engine/src/conversion/downcasthelpers.js
  5. 0 609
      packages/ckeditor5-engine/src/conversion/upcast-converters.js
  6. 720 0
      packages/ckeditor5-engine/src/conversion/upcasthelpers.js
  7. 50 11
      packages/ckeditor5-engine/src/dev-utils/model.js
  8. 2 2
      packages/ckeditor5-engine/src/model/markercollection.js
  9. 24 29
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  10. 6 5
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  11. 57 13
      packages/ckeditor5-engine/tests/conversion/conversion.js
  12. 17 23
      packages/ckeditor5-engine/tests/conversion/downcast-selection-converters.js
  13. 864 181
      packages/ckeditor5-engine/tests/conversion/downcasthelpers.js
  14. 76 129
      packages/ckeditor5-engine/tests/conversion/upcasthelpers.js
  15. 22 0
      packages/ckeditor5-engine/tests/dev-utils/model.js
  16. 8 18
      packages/ckeditor5-engine/tests/manual/highlight.js
  17. 4 8
      packages/ckeditor5-engine/tests/manual/markers.js
  18. 8 16
      packages/ckeditor5-engine/tests/manual/nestededitable.js
  19. 8 11
      packages/ckeditor5-engine/tests/manual/selection.js
  20. 4 12
      packages/ckeditor5-engine/tests/manual/tickets/475/1.js
  21. 4 5
      packages/ckeditor5-engine/tests/manual/tickets/ckeditor5-721/1.js
  22. 4 12
      packages/ckeditor5-engine/tests/tickets/699.js
  23. 3 4
      packages/ckeditor5-engine/tests/utils/bindtwostepcarettoattribute.js

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

@@ -14,10 +14,10 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Mapper from '../conversion/mapper';
 
 import DowncastDispatcher from '../conversion/downcastdispatcher';
-import { insertText } from '../conversion/downcast-converters';
+import { insertText } from '../conversion/downcasthelpers';
 
 import UpcastDispatcher from '../conversion/upcastdispatcher';
-import { convertText, convertToModelFragment } from '../conversion/upcast-converters';
+import { convertText, convertToModelFragment } from '../conversion/upcasthelpers';
 
 import ViewDocumentFragment from '../view/documentfragment';
 import ViewDocument from '../view/document';

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

@@ -11,7 +11,7 @@ import RootEditableElement from '../view/rooteditableelement';
 import View from '../view/view';
 import Mapper from '../conversion/mapper';
 import DowncastDispatcher from '../conversion/downcastdispatcher';
-import { insertText, remove } from '../conversion/downcast-converters';
+import { insertText, remove } from '../conversion/downcasthelpers';
 import { convertSelectionChange } from '../conversion/upcast-selection-converters';
 import { clearAttributes, convertCollapsedSelection, convertRangeSelection } from '../conversion/downcast-selection-converters';
 

+ 105 - 86
packages/ckeditor5-engine/src/conversion/conversion.js

@@ -9,18 +9,6 @@
 
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
-import {
-	downcastElementToElement,
-	downcastAttributeToElement,
-	downcastAttributeToAttribute
-} from './downcast-converters';
-
-import {
-	upcastElementToElement,
-	upcastElementToAttribute,
-	upcastAttributeToAttribute
-} from './upcast-converters';
-
 /**
  * A utility class that helps add converters to upcast and downcast dispatchers.
  *
@@ -40,17 +28,18 @@ import {
  * method:
  *
  *		// Add a converter to editing downcast and data downcast.
- *		editor.conversion.for( 'downcast' ).add( downcastElementToElement( config ) );
+ *		editor.conversion.for( 'downcast' ).elementToElement( config ) );
  *
  *		// Add a converter to the data pipepline only:
- *		editor.conversion.for( 'dataDowncast' ).add( downcastElementToElement( dataConversionConfig ) );
+ *		editor.conversion.for( 'dataDowncast' ).elementToElement( dataConversionConfig ) );
+ *
  *		// And a slightly different one for the editing pipeline:
- *		editor.conversion.for( 'editingDowncast' ).add( downcastElementToElement( editingConversionConfig ) );
+ *		editor.conversion.for( 'editingDowncast' ).elementToElement( editingConversionConfig ) );
  *
  * The functions used in `add()` calls are one-way converters (i.e. you need to remember yourself to add
  * a converter in the other direction, if your feature requires that). They are also called "conversion helpers".
- * You can find a set of them in the {@link module:engine/conversion/downcast-converters} and
- * {@link module:engine/conversion/upcast-converters} modules.
+ * You can find a set of them in the {@link module:engine/conversion/downcasthelpers} and
+ * {@link module:engine/conversion/upcasthelpers} modules.
  *
  * Besides allowing to register converters to specific dispatchers, you can also use methods available in this
  * class to add two-way converters (upcast and downcast):
@@ -81,13 +70,17 @@ export default class Conversion {
 	 * If a given group name is used for the second time, the
 	 * {@link module:utils/ckeditorerror~CKEditorError `conversion-register-group-exists` error} is thrown.
 	 *
-	 * @param {String} groupName The name for dispatchers group.
-	 * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
-	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers Dispatchers to register
+	 * @param {Object} options
+	 * @param {String} options.name The name for dispatchers group.
+	 * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher|Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} options.dispatcher Dispatcher or array of dispatchers to register
 	 * under the given name.
+	 * @param {module:engine/conversion/downcasthelpers~DowncastHelpers|
+	 * module:engine/conversion/upcasthelpers~UpcastHelpers} helpers
 	 */
-	register( groupName, dispatchers ) {
-		if ( this._dispatchersGroups.has( groupName ) ) {
+	register( name, group ) {
+		if ( this._dispatchersGroups.has( name ) ) {
 			/**
 			 * Trying to register a group name that was already registered.
 			 *
@@ -96,16 +89,18 @@ export default class Conversion {
 			throw new CKEditorError( 'conversion-register-group-exists: Trying to register a group name that was already registered.' );
 		}
 
-		this._dispatchersGroups.set( groupName, dispatchers );
+		this._dispatchersGroups.set( name, group );
 	}
 
 	/**
 	 * Provides chainable API to assign converters to dispatchers registered under a given group name. Converters are added
-	 * by calling the `.add()` method of an object returned by this function.
+	 * by calling the {@link module:engine/conversion/conversion~ConversionHelpers#add `.add()`} method of an
+	 * {@link module:engine/conversion/conversion~ConversionHelpers conversion helpers} returned by this function.
 	 *
-	 *		conversion.for( 'downcast' )
-	 *			.add( conversionHelperA )
-	 *			.add( conversionHelperB );
+	 *		editor.conversion.for( 'downcast' )
+	 *			.add( conversionHelperA ) // Adds a custom converter A.
+	 *			.add( conversionHelperB ) // Adds a custom converter B.
+	 *			.elementToElement( config ); // Adds a custom element-to-element downcast converter.
 	 *
 	 * In this example `conversionHelperA` and `conversionHelperB` will be called for all dispatchers from the `'model'` group.
 	 *
@@ -117,15 +112,18 @@ export default class Conversion {
 	 *
 	 * For downcast (model-to-view conversion), these are:
 	 *
-	 * * {@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}.
+	 * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement Downcast element-to-element converter},
+	 * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement Downcast attribute-to-element converter},
+	 * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToAttribute Downcast attribute-to-attribute converter}.
+	 * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToElement Downcast marker-to-element converter}.
+	 * * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToHighlight Downcast marker-to-highlight converter}.
 	 *
 	 * For upcast (view-to-model conversion), these are:
 	 *
-	 * * {@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}.
+	 * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToElement Upcast element-to-element converter},
+	 * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute Upcast attribute-to-element converter},
+	 * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute Upcast attribute-to-attribute converter}.
+	 * * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToMarker Upcast element-to-marker converter}.
 	 *
 	 * An example of using conversion helpers to convert the `paragraph` model element to the `p` view element (and back):
 	 *
@@ -133,27 +131,16 @@ export default class Conversion {
 	 *		const config = { model: 'paragraph', view: 'p' };
 	 *
 	 *		// Add converters to proper dispatchers using conversion helpers.
-	 *		conversion.for( 'downcast' ).add( downcastElementToElement( config ) );
-	 *		conversion.for( 'upcast' ).add( upcastElementToElement( config ) );
-	 *
-	 * An example of providing a custom conversion helper that uses a custom converter function:
-	 *
-	 *		// Adding a custom `myConverter` converter for 'paragraph' element insertion, with the default priority ('normal').
-	 *		conversion.for( 'downcast' ).add( conversion.customConverter( 'insert:paragraph', myConverter ) );
+	 *		editor.conversion.for( 'downcast' ).elementToElement( config ) );
+	 *		editor.conversion.for( 'upcast' ).elementToElement( config ) );
 	 *
 	 * @param {String} groupName The name of dispatchers group to add the converters to.
-	 * @returns {Object} An object with the `.add()` method, providing a way to add converters.
+	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers}
 	 */
 	for( groupName ) {
-		const dispatchers = this._getDispatchers( groupName );
-
-		return {
-			add( conversionHelper ) {
-				_addToDispatchers( dispatchers, conversionHelper );
+		const group = this._getDispatchersGroup( groupName );
 
-				return this;
-			}
-		};
+		return group;
 	}
 
 	/**
@@ -229,17 +216,16 @@ export default class Conversion {
 	 */
 	elementToElement( definition ) {
 		// Set up downcast converter.
-		this.for( 'downcast' ).add( downcastElementToElement( definition ) );
+		this.for( 'downcast' ).elementToElement( definition );
 
 		// Set up upcast converter.
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
-			this.for( 'upcast' ).add(
-				upcastElementToElement( {
+			this.for( 'upcast' )
+				.elementToElement( {
 					model,
 					view,
 					converterPriority: definition.converterPriority
-				} )
-			);
+				} );
 		}
 	}
 
@@ -402,17 +388,16 @@ export default class Conversion {
 	 */
 	attributeToElement( definition ) {
 		// Set up downcast converter.
-		this.for( 'downcast' ).add( downcastAttributeToElement( definition ) );
+		this.for( 'downcast' ).attributeToElement( definition );
 
 		// Set up upcast converter.
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
-			this.for( 'upcast' ).add(
-				upcastElementToAttribute( {
+			this.for( 'upcast' )
+				.elementToAttribute( {
 					view,
 					model,
 					converterPriority: definition.priority
-				} )
-			);
+				} );
 		}
 	}
 
@@ -528,34 +513,30 @@ export default class Conversion {
 	 */
 	attributeToAttribute( definition ) {
 		// Set up downcast converter.
-		this.for( 'downcast' ).add( downcastAttributeToAttribute( definition ) );
+		this.for( 'downcast' ).attributeToAttribute( definition );
 
 		// Set up upcast converter.
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
-			this.for( 'upcast' ).add(
-				upcastAttributeToAttribute( {
+			this.for( 'upcast' )
+				.attributeToAttribute( {
 					view,
 					model
-				} )
-			);
+				} );
 		}
 	}
 
 	/**
-	 * Returns dispatchers registered under a given group name.
+	 * Returns dispatchers group registered under a given group name.
 	 *
 	 * If the given group name has not been registered, the
 	 * {@link module:utils/ckeditorerror~CKEditorError `conversion-for-unknown-group` error} is thrown.
 	 *
 	 * @private
 	 * @param {String} groupName
-	 * @returns {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
-	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>}
+	 * @returns {module:engine/conversion/conversion~DispatchersGroup}
 	 */
-	_getDispatchers( groupName ) {
-		const dispatchers = this._dispatchersGroups.get( groupName );
-
-		if ( !dispatchers ) {
+	_getDispatchersGroup( groupName ) {
+		if ( !this._dispatchersGroups.has( groupName ) ) {
 			/**
 			 * Trying to add a converter to an unknown dispatchers group.
 			 *
@@ -564,7 +545,7 @@ export default class Conversion {
 			throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' );
 		}
 
-		return dispatchers;
+		return this._dispatchersGroups.get( groupName );
 	}
 }
 
@@ -585,20 +566,13 @@ export default class Conversion {
  * @property {module:utils/priorities~PriorityString} [converterPriority] The converter priority.
  */
 
-// Helper function for the `Conversion` `.add()` method.
-//
-// Calls `conversionHelper` on each dispatcher from the group specified earlier in the `.for()` call, effectively
-// adding converters to all specified dispatchers.
-//
-// @private
-// @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
-// module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers
-// @param {Function} conversionHelper
-function _addToDispatchers( dispatchers, conversionHelper ) {
-	for ( const dispatcher of dispatchers ) {
-		conversionHelper( dispatcher );
-	}
-}
+/**
+ * @typedef {Object} module:engine/conversion/conversion~DispatchersGroup
+ * @property {String} name Group name
+ * @property {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+ * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers
+ * @property {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers} helpers
+ */
 
 // Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
 // `definition.upcastAlso`.
@@ -630,3 +604,48 @@ function* _getUpcastDefinition( model, view, upcastAlso ) {
 		}
 	}
 }
+
+/**
+ * Base class for conversion helpers.
+ */
+export class ConversionHelpers {
+	/**
+	 * Creates ConversionHelpers instance.
+	 *
+	 * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatcher
+	 */
+	constructor( dispatcher ) {
+		this._dispatchers = Array.isArray( dispatcher ) ? dispatcher : [ dispatcher ];
+	}
+
+	/**
+	 * Registers a conversion helper.
+	 *
+	 * **Note**: See full usage example in the `{@link module:engine/conversion/conversion~Conversion#for conversion.for()}`
+	 * method description
+	 *
+	 * @param {Function} conversionHelper The function to be called on event.
+	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers}
+	 */
+	add( conversionHelper ) {
+		this._addToDispatchers( conversionHelper );
+
+		return this;
+	}
+
+	/**
+	 * Helper function for the `Conversion` `.add()` method.
+	 *
+	 * Calls `conversionHelper` on each dispatcher from the group specified earlier in the `.for()` call, effectively
+	 * adding converters to all specified dispatchers.
+	 *
+	 * @private
+	 * @param {Function} conversionHelper
+	 */
+	_addToDispatchers( conversionHelper ) {
+		for ( const dispatcher of this._dispatchers ) {
+			conversionHelper( dispatcher );
+		}
+	}
+}

Разлика између датотеке није приказан због своје велике величине
+ 452 - 470
packages/ckeditor5-engine/src/conversion/downcasthelpers.js


+ 0 - 609
packages/ckeditor5-engine/src/conversion/upcast-converters.js

@@ -1,609 +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 { cloneDeep } from 'lodash-es';
-
-/**
- * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
- * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
- *
- * @module engine/conversion/upcast-converters
- */
-
-/**
- * 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', converterPriority: 'high' } );
- *
- *		upcastElementToElement( {
- *			view: {
- *				name: 'p',
- *				classes: 'fancy'
- *			},
- *			model: 'fancyParagraph'
- *		} );
- *
- *		upcastElementToElement( {
- * 			view: {
- *				name: 'p',
- *				classes: 'heading'
- * 			},
- * 			model: ( viewElement, modelWriter ) => {
- * 				return modelWriter.createElement( '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} [config.converterPriority='normal'] Converter priority.
- * @returns {Function} Conversion helper.
- */
-export function upcastElementToElement( config ) {
-	config = cloneDeep( config );
-
-	const converter = _prepareToElementConverter( config );
-
-	const elementName = _getViewElementNameFromConfig( config );
-	const eventName = elementName ? 'element:' + elementName : 'element';
-
-	return dispatcher => {
-		dispatcher.on( eventName, converter, { priority: config.converterPriority || 'normal' } );
-	};
-}
-
-/**
- * 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`.
- *
- * This helper is meant to set a model attribute on all the elements that are inside the converted element:
- *
- *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
- *
- * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
- * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text. See
- * {@link module:engine/conversion/upcast-converters~upcastAttributeToAttribute} for comparison.
- *
- * 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', converterPriority: 'high' } );
- *
- *		upcastElementToAttribute( {
- *			view: {
- *				name: 'span',
- *				classes: 'bold'
- *			},
- *			model: 'bold'
- *		} );
- *
- *		upcastElementToAttribute( {
- *			view: {
- *				name: 'span',
- *				classes: [ 'styled', 'styled-dark' ]
- *			},
- *			model: {
- *				key: 'styled',
- *				value: 'dark'
- *			}
- *		} );
- *
- * 		upcastElementToAttribute( {
- *			view: {
- *				name: 'span',
- *				styles: {
- *					'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} [config.converterPriority='normal'] Converter priority.
- * @returns {Function} Conversion helper.
- */
-export function upcastElementToAttribute( config ) {
-	config = cloneDeep( config );
-
-	_normalizeModelAttributeConfig( config );
-
-	const converter = _prepareToAttributeConverter( config, false );
-
-	const elementName = _getViewElementNameFromConfig( config );
-	const eventName = elementName ? 'element:' + elementName : 'element';
-
-	return dispatcher => {
-		dispatcher.on( eventName, converter, { priority: config.converterPriority || 'low' } );
-	};
-}
-
-/**
- * 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.
- *
- * This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute
- * is set only on the corresponding model node:
- *
- *		<div class="dark"><div>foo</div></div>    -->    <div dark="true"><div>foo</div></div>
- *
- * Above, `class="dark"` attribute is added only to the `<div>` elements that has it. This is in contrary to
- * {@link module:engine/conversion/upcast-converters~upcastElementToAttribute} which sets attributes for all the children in the model:
- *
- *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
- *
- * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
- * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text.
- *
- * 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', converterPriority: 'normal' } );
- *
- *		upcastAttributeToAttribute( {
- *			view: {
- *				key: 'data-style',
- *				value: /[\s\S]+/
- *			},
- *			model: 'styled'
- *		} );
- *
- *		upcastAttributeToAttribute( {
- *			view: {
- *				name: 'img',
- *				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 {Object} config Conversion configuration.
- * @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} [config.converterPriority='low'] Converter priority.
- * @returns {Function} Conversion helper.
- */
-export function upcastAttributeToAttribute( config ) {
-	config = cloneDeep( config );
-
-	let viewKey = null;
-
-	if ( typeof config.view == 'string' || config.view.key ) {
-		viewKey = _normalizeViewAttributeKeyValueConfig( config );
-	}
-
-	_normalizeModelAttributeConfig( config, viewKey );
-
-	const converter = _prepareToAttributeConverter( config, true );
-
-	return dispatcher => {
-		dispatcher.on( 'element', converter, { priority: config.converterPriority || 'low' } );
-	};
-}
-
-/**
- * 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', converterPriority: 'high' } );
- *
- *		upcastElementToMarker( {
- *			view: 'marker-search',
- *			model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
- *		} );
- *
- *		upcastElementToMarker( {
- *			view: {
- *				name: 'span',
- *				attributes: {
- *					'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} [config.converterPriority='normal'] Converter priority.
- * @returns {Function} Conversion helper.
- */
-export function upcastElementToMarker( config ) {
-	config = cloneDeep( config );
-
-	_normalizeToMarkerConfig( config );
-
-	return upcastElementToElement( config );
-}
-
-// 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;
-		}
-
-		// Force consuming element's name.
-		match.match.name = true;
-
-		// 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.modelCursor );
-
-		// 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, conversionApi.writer.createPositionAt( modelElement, 0 ) );
-
-		// 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
-			conversionApi.writer.createPositionBefore( 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>
-			conversionApi.writer.createPositionAfter( childrenResult.modelCursor.parent )
-		);
-
-		// Now we need to check where the modelCursor 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.modelCursor = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 );
-
-			// Otherwise just continue after inserted element.
-		} else {
-			data.modelCursor = 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 {
-		return writer.createElement( 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;
-	let normalized;
-
-	if ( key == 'class' || key == 'style' ) {
-		const keyName = key == 'class' ? 'classes' : 'styles';
-
-		normalized = {
-			[ keyName ]: config.view.value
-		};
-	} else {
-		const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
-
-		normalized = {
-			attributes: {
-				[ 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' || typeof config.model.value == 'undefined' ? 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} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
-// on all elements in the range.
-function _prepareToAttributeConverter( config, shallow ) {
-	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 ( _onlyViewNameIsDefined( config ) ) {
-			match.match.name = true;
-		} else {
-			// 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.modelCursor ) );
-		}
-
-		// Set attribute on current `output`. `Schema` is checked inside this helper function.
-		const attributeWasSet = _setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, shallow, conversionApi );
-
-		if ( attributeWasSet ) {
-			conversionApi.consumable.consume( data.viewItem, match.match );
-		}
-	};
-}
-
-// Helper function that checks if element name should be consumed in attribute converters.
-//
-// @param {Object} config Conversion config.
-// @returns {Boolean}
-function _onlyViewNameIsDefined( config ) {
-	if ( typeof config.view == 'object' && !_getViewElementNameFromConfig( config ) ) {
-		return false;
-	}
-
-	return !config.view.classes && !config.view.attributes && !config.view.styles;
-}
-
-// 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.
-// @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
-// on all elements in the range.
-// @returns {Boolean} `true` if attribute was set on at least one node from given `modelRange`.
-function _setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) {
-	let result = false;
-
-	// Set attribute on each item in range according to Schema.
-	for ( const node of Array.from( modelRange.getItems( { shallow } ) ) ) {
-		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, modelWriter ) => {
-		const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement );
-
-		return modelWriter.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}.
- * This is the "entry-point" converter for upcast (view to model conversion). This converter starts the conversion of all children
- * of passed view document fragment. Those children {@link module:engine/view/node~Node view nodes} are then handled by other converters.
- *
- * This also a "default", last resort converter for all view elements that has not been converted by other converters.
- * When a view element is being converted to the model but it does not have converter specified, that view element
- * will be converted to {@link module:engine/model/documentfragment~DocumentFragment model document fragment} and returned.
- *
- * @returns {Function} Universal converter for view {@link module:engine/view/documentfragment~DocumentFragment fragments} and
- * {@link module:engine/view/element~Element elements} that returns
- * {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item.
- */
-export function convertToModelFragment() {
-	return ( evt, data, conversionApi ) => {
-		// Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
-		if ( !data.modelRange && conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
-			const { modelRange, modelCursor } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
-
-			data.modelRange = modelRange;
-			data.modelCursor = modelCursor;
-		}
-	};
-}
-
-/**
- * Function factory, creates a converter that converts {@link module:engine/view/text~Text} to {@link module:engine/model/text~Text}.
- *
- * @returns {Function} {@link module:engine/view/text~Text View text} converter.
- */
-export function convertText() {
-	return ( evt, data, conversionApi ) => {
-		if ( conversionApi.schema.checkChild( data.modelCursor, '$text' ) ) {
-			if ( conversionApi.consumable.consume( data.viewItem ) ) {
-				const text = conversionApi.writer.createText( data.viewItem.data );
-
-				conversionApi.writer.insert( text, data.modelCursor );
-
-				data.modelRange = ModelRange._createFromPositionAndShift( data.modelCursor, text.offsetSize );
-				data.modelCursor = data.modelRange.end;
-			}
-		}
-	};
-}

+ 720 - 0
packages/ckeditor5-engine/src/conversion/upcasthelpers.js

@@ -0,0 +1,720 @@
+/**
+ * @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 { ConversionHelpers } from './conversion';
+
+import { cloneDeep } from 'lodash-es';
+
+/**
+ * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
+ * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
+ *
+ * @module engine/conversion/upcasthelpers
+ */
+
+/**
+ * Upcast conversion helper functions.
+ *
+ * @extends module:engine/conversion/conversion~ConversionHelpers
+ */
+export default class UpcastHelpers extends ConversionHelpers {
+	/**
+	 * 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.
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToElement( {
+	 *			view: 'p',
+	 *			model: 'paragraph'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToElement( {
+	 *			view: 'p',
+	 *			model: 'paragraph',
+	 *			converterPriority: 'high'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToElement( {
+	 *			view: {
+	 *				name: 'p',
+	 *				classes: 'fancy'
+	 *			},
+	 *			model: 'fancyParagraph'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToElement( {
+	 * 			view: {
+	 *				name: 'p',
+	 *				classes: 'heading'
+	 * 			},
+	 * 			model: ( viewElement, modelWriter ) => {
+	 * 				return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
+	 * 			}
+	 * 		} );
+	 *
+	 * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #elementToElement
+	 * @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} [config.converterPriority='normal'] Converter priority.
+	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
+	 */
+	elementToElement( config ) {
+		return this.add( upcastElementToElement( config ) );
+	}
+
+	/**
+	 * 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`.
+	 *
+	 * This helper is meant to set a model attribute on all the elements that are inside the converted element:
+	 *
+	 *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
+	 *
+	 * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
+	 * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text. See
+	 * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute} for comparison.
+	 *
+	 * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToAttribute( {
+	 *			view: 'strong',
+	 *			model: 'bold'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToAttribute( {
+	 *			view: 'strong',
+	 *			model: 'bold',
+	 *			converterPriority: 'high'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToAttribute( {
+	 *			view: {
+	 *				name: 'span',
+	 *				classes: 'bold'
+	 *			},
+	 *			model: 'bold'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToAttribute( {
+	 *			view: {
+	 *				name: 'span',
+	 *				classes: [ 'styled', 'styled-dark' ]
+	 *			},
+	 *			model: {
+	 *				key: 'styled',
+	 *				value: 'dark'
+	 *			}
+	 *		} );
+	 *
+	 * 		editor.conversion.for( 'upcast' ).elementToAttribute( {
+	 *			view: {
+	 *				name: 'span',
+	 *				styles: {
+	 *					'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 `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #elementToAttribute
+	 * @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} [config.converterPriority='normal'] Converter priority.
+	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
+	 */
+	elementToAttribute( config ) {
+		return this.add( upcastElementToAttribute( config ) );
+	}
+
+	/**
+	 * 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.
+	 *
+	 * This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute
+	 * is set only on the corresponding model node:
+	 *
+	 *		<div class="dark"><div>foo</div></div>    -->    <div dark="true"><div>foo</div></div>
+	 *
+	 * Above, `class="dark"` attribute is added only to the `<div>` elements that has it. This is in contrary to
+	 * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute} which sets attributes for
+	 * all the children in the model:
+	 *
+	 *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
+	 *
+	 * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
+	 * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text.
+	 *
+	 * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			view: 'src',
+	 *			model: 'source'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			view: { key: 'src' },
+	 *			model: 'source'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			view: { key: 'src' },
+	 *			model: 'source',
+	 *			converterPriority: 'normal'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			view: {
+	 *				key: 'data-style',
+	 *				value: /[\s\S]+/
+	 *			},
+	 *			model: 'styled'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			view: {
+	 *				name: 'img',
+	 *				key: 'class',
+	 *				value: 'styled-dark'
+	 *			},
+	 *			model: {
+	 *				key: 'styled',
+	 *				value: 'dark'
+	 *			}
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			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 `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #attributeToAttribute
+	 * @param {Object} config Conversion configuration.
+	 * @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} [config.converterPriority='low'] Converter priority.
+	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
+	 */
+	attributeToAttribute( config ) {
+		return this.add( upcastAttributeToAttribute( config ) );
+	}
+
+	/**
+	 * 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}.
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToMarker( {
+	 *			view: 'marker-search',
+	 *			model: 'search'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToMarker( {
+	 *			view: 'marker-search',
+	 *			model: 'search',
+	 *			converterPriority: 'high'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToMarker( {
+	 *			view: 'marker-search',
+	 *			model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToMarker( {
+	 *			view: {
+	 *				name: 'span',
+	 *				attributes: {
+	 *					'data-marker': 'search'
+	 *				}
+	 *			},
+	 *			model: 'search'
+	 *		} );
+	 *
+	 * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #elementToMarker
+	 * @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} [config.converterPriority='normal'] Converter priority.
+	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
+	 */
+	elementToMarker( config ) {
+		return this.add( upcastElementToMarker( config ) );
+	}
+}
+
+/**
+ * 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}.
+ * This is the "entry-point" converter for upcast (view to model conversion). This converter starts the conversion of all children
+ * of passed view document fragment. Those children {@link module:engine/view/node~Node view nodes} are then handled by other converters.
+ *
+ * This also a "default", last resort converter for all view elements that has not been converted by other converters.
+ * When a view element is being converted to the model but it does not have converter specified, that view element
+ * will be converted to {@link module:engine/model/documentfragment~DocumentFragment model document fragment} and returned.
+ *
+ * @returns {Function} Universal converter for view {@link module:engine/view/documentfragment~DocumentFragment fragments} and
+ * {@link module:engine/view/element~Element elements} that returns
+ * {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item.
+ */
+export function convertToModelFragment() {
+	return ( evt, data, conversionApi ) => {
+		// Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
+		if ( !data.modelRange && conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
+			const { modelRange, modelCursor } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
+
+			data.modelRange = modelRange;
+			data.modelCursor = modelCursor;
+		}
+	};
+}
+
+/**
+ * Function factory, creates a converter that converts {@link module:engine/view/text~Text} to {@link module:engine/model/text~Text}.
+ *
+ * @returns {Function} {@link module:engine/view/text~Text View text} converter.
+ */
+export function convertText() {
+	return ( evt, data, conversionApi ) => {
+		if ( conversionApi.schema.checkChild( data.modelCursor, '$text' ) ) {
+			if ( conversionApi.consumable.consume( data.viewItem ) ) {
+				const text = conversionApi.writer.createText( data.viewItem.data );
+
+				conversionApi.writer.insert( text, data.modelCursor );
+
+				data.modelRange = ModelRange._createFromPositionAndShift( data.modelCursor, text.offsetSize );
+				data.modelCursor = data.modelRange.end;
+			}
+		}
+	};
+}
+
+// View element to model element conversion helper.
+//
+// See {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper} for examples.
+//
+// @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} [config.converterPriority='normal'] Converter priority.
+// @returns {Function} Conversion helper.
+function upcastElementToElement( config ) {
+	config = cloneDeep( config );
+
+	const converter = prepareToElementConverter( config );
+
+	const elementName = getViewElementNameFromConfig( config );
+	const eventName = elementName ? 'element:' + elementName : 'element';
+
+	return dispatcher => {
+		dispatcher.on( eventName, converter, { priority: config.converterPriority || 'normal' } );
+	};
+}
+
+// View element to model attribute conversion helper.
+//
+// See {@link ~UpcastHelpers#elementToAttribute `.elementToAttribute()` upcast helper} for examples.
+//
+// @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} [config.converterPriority='normal'] Converter priority.
+// @returns {Function} Conversion helper.
+function upcastElementToAttribute( config ) {
+	config = cloneDeep( config );
+
+	normalizeModelAttributeConfig( config );
+
+	const converter = prepareToAttributeConverter( config, false );
+
+	const elementName = getViewElementNameFromConfig( config );
+	const eventName = elementName ? 'element:' + elementName : 'element';
+
+	return dispatcher => {
+		dispatcher.on( eventName, converter, { priority: config.converterPriority || 'low' } );
+	};
+}
+
+// View attribute to model attribute conversion helper.
+//
+// See {@link ~UpcastHelpers#attributeToAttribute `.attributeToAttribute()` upcast helper} for examples.
+//
+// @param {Object} config Conversion configuration.
+// @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} [config.converterPriority='low'] Converter priority.
+// @returns {Function} Conversion helper.
+function upcastAttributeToAttribute( config ) {
+	config = cloneDeep( config );
+
+	let viewKey = null;
+
+	if ( typeof config.view == 'string' || config.view.key ) {
+		viewKey = normalizeViewAttributeKeyValueConfig( config );
+	}
+
+	normalizeModelAttributeConfig( config, viewKey );
+
+	const converter = prepareToAttributeConverter( config, true );
+
+	return dispatcher => {
+		dispatcher.on( 'element', converter, { priority: config.converterPriority || 'low' } );
+	};
+}
+
+// View element to model marker conversion helper.
+//
+// See {@link ~UpcastHelpers#elementToMarker `.elementToMarker()` upcast helper} for examples.
+//
+// @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} [config.converterPriority='normal'] Converter priority.
+// @returns {Function} Conversion helper.
+function upcastElementToMarker( config ) {
+	config = cloneDeep( config );
+
+	normalizeToMarkerConfig( config );
+
+	return upcastElementToElement( config );
+}
+
+// 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;
+		}
+
+		// Force consuming element's name.
+		match.match.name = true;
+
+		// 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.modelCursor );
+
+		// 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, conversionApi.writer.createPositionAt( modelElement, 0 ) );
+
+		// 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
+			conversionApi.writer.createPositionBefore( 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>
+			conversionApi.writer.createPositionAfter( childrenResult.modelCursor.parent )
+		);
+
+		// Now we need to check where the modelCursor 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.modelCursor = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 );
+
+			// Otherwise just continue after inserted element.
+		} else {
+			data.modelCursor = 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 {
+		return writer.createElement( 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;
+	let normalized;
+
+	if ( key == 'class' || key == 'style' ) {
+		const keyName = key == 'class' ? 'classes' : 'styles';
+
+		normalized = {
+			[ keyName ]: config.view.value
+		};
+	} else {
+		const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
+
+		normalized = {
+			attributes: {
+				[ 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' || typeof config.model.value == 'undefined' ? 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} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
+// on all elements in the range.
+function prepareToAttributeConverter( config, shallow ) {
+	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 ( onlyViewNameIsDefined( config ) ) {
+			match.match.name = true;
+		} else {
+			// 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.modelCursor ) );
+		}
+
+		// Set attribute on current `output`. `Schema` is checked inside this helper function.
+		const attributeWasSet = setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, shallow, conversionApi );
+
+		if ( attributeWasSet ) {
+			conversionApi.consumable.consume( data.viewItem, match.match );
+		}
+	};
+}
+
+// Helper function that checks if element name should be consumed in attribute converters.
+//
+// @param {Object} config Conversion config.
+// @returns {Boolean}
+function onlyViewNameIsDefined( config ) {
+	if ( typeof config.view == 'object' && !getViewElementNameFromConfig( config ) ) {
+		return false;
+	}
+
+	return !config.view.classes && !config.view.attributes && !config.view.styles;
+}
+
+// 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.
+// @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
+// on all elements in the range.
+// @returns {Boolean} `true` if attribute was set on at least one node from given `modelRange`.
+function setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) {
+	let result = false;
+
+	// Set attribute on each item in range according to Schema.
+	for ( const node of Array.from( modelRange.getItems( { shallow } ) ) ) {
+		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, modelWriter ) => {
+		const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement );
+
+		return modelWriter.createElement( '$marker', { 'data-name': markerName } );
+	};
+}

+ 50 - 11
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -32,7 +32,7 @@ import {
 	convertRangeSelection,
 	convertCollapsedSelection,
 } from '../conversion/downcast-selection-converters';
-import { insertText, insertElement, wrap, insertUIElement } from '../conversion/downcast-converters';
+import { insertText, wrap } from '../conversion/downcasthelpers';
 
 import { isPlainObject } from 'lodash-es';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
@@ -225,19 +225,59 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
 			converter( evt, data, conversionApi );
 		}
 	} );
-	downcastDispatcher.on( 'insert', insertElement( modelItem => {
-		// Stringify object types values for properly display as an output string.
-		const attributes = convertAttributes( modelItem.getAttributes(), stringifyAttributeValue );
+	downcastDispatcher.on( 'insert', ( evt, data, conversionApi ) => {
+		const attributes = convertAttributes( data.item.getAttributes(), stringifyAttributeValue );
+		const viewElement = new ViewContainerElement( data.item.name, attributes );
+
+		if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
+			return;
+		}
+
+		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
+
+		conversionApi.mapper.bindElements( data.item, viewElement );
+		conversionApi.writer.insert( viewPosition, viewElement );
+	} );
 
-		return new ViewContainerElement( modelItem.name, attributes );
-	} ) );
 	downcastDispatcher.on( 'selection', convertRangeSelection() );
 	downcastDispatcher.on( 'selection', convertCollapsedSelection() );
-	downcastDispatcher.on( 'addMarker', insertUIElement( ( data, writer ) => {
-		const name = data.markerName + ':' + ( data.isOpening ? 'start' : 'end' );
+	downcastDispatcher.on( 'addMarker', ( evt, data, conversionApi ) => {
+		const elementCreator = ( data, writer ) => {
+			const name = data.markerName + ':' + ( data.isOpening ? 'start' : 'end' );
+
+			return writer.createUIElement( name );
+		};
+
+		// Create two view elements. One will be inserted at the beginning of marker, one at the end.
+		// If marker is collapsed, only "opening" element will be inserted.
+		data.isOpening = true;
+		const viewStartElement = elementCreator( data, conversionApi.writer );
+
+		data.isOpening = false;
+		const viewEndElement = elementCreator( data, conversionApi.writer );
+
+		const markerRange = data.markerRange;
 
-		return writer.createUIElement( name );
-	} ) );
+		// If marker's range is not collapsed - consume all items inside.
+		for ( const value of markerRange ) {
+			conversionApi.consumable.consume( value.item, evt.name );
+		}
+
+		const mapper = conversionApi.mapper;
+		const viewWriter = conversionApi.writer;
+
+		// Add "opening" element.
+		viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
+		conversionApi.mapper.bindElementToMarker( viewStartElement, data.markerName );
+
+		// Add "closing" element only if range is not collapsed.
+		if ( !markerRange.isCollapsed ) {
+			viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
+			conversionApi.mapper.bindElementToMarker( viewEndElement, data.markerName );
+		}
+
+		evt.stop();
+	} );
 
 	// Convert model to view.
 	const writer = view._writer;
@@ -278,7 +318,6 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
  *
  * @param {String} data HTML-like string to be parsed.
  * @param {module:engine/model/schema~Schema} schema A schema instance used by converters for element validation.
- * @param {module:engine/model/batch~Batch} batch A batch used for conversion.
  * @param {Object} [options={}] Additional configuration.
  * @param {Array<Object>} [options.selectionAttributes] A list of attributes which will be passed to the selection.
  * @param {Boolean} [options.lastRangeBackward=false] If set to `true`, the last range will be added as backward.

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

@@ -300,9 +300,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-converters downcast converters} or attach a custom converter to mentioned events.
+ * Use {@link module:engine/conversion/downcasthelpers 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-converters upcast converters} or
+ * Then, it can be upcasted back to a marker. Again, use {@link module:engine/conversion/upcasthelpers 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

+ 24 - 29
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -18,19 +18,11 @@ import { parse as parseView, stringify as stringifyView } from '../../src/dev-ut
 
 import count from '@ckeditor/ckeditor5-utils/src/count';
 
-import {
-	upcastElementToElement,
-	upcastElementToAttribute
-} from '../../src/conversion/upcast-converters';
-
-import {
-	downcastElementToElement,
-	downcastAttributeToElement,
-	downcastMarkerToHighlight
-} from '../../src/conversion/downcast-converters';
+import UpcastHelpers from '../../src/conversion/upcasthelpers';
+import DowncastHelpers from '../../src/conversion/downcasthelpers';
 
 describe( 'DataController', () => {
-	let model, modelDocument, htmlDataProcessor, data, schema;
+	let model, modelDocument, htmlDataProcessor, data, schema, upcastHelpers, downcastHelpers;
 
 	beforeEach( () => {
 		model = new Model();
@@ -46,6 +38,9 @@ describe( 'DataController', () => {
 		htmlDataProcessor = new HtmlDataProcessor();
 
 		data = new DataController( model, htmlDataProcessor );
+
+		upcastHelpers = new UpcastHelpers( data.upcastDispatcher );
+		downcastHelpers = new DowncastHelpers( data.downcastDispatcher );
 	} );
 
 	describe( 'constructor()', () => {
@@ -68,7 +63,7 @@ describe( 'DataController', () => {
 		it( 'should set paragraph', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
-			upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher );
+			upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );
 
 			const output = data.parse( '<p>foo<b>bar</b></p>' );
 
@@ -79,7 +74,7 @@ describe( 'DataController', () => {
 		it( 'should set two paragraphs', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
-			upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher );
+			upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );
 
 			const output = data.parse( '<p>foo</p><p>bar</p>' );
 
@@ -93,8 +88,8 @@ describe( 'DataController', () => {
 				allowAttributes: [ 'bold' ]
 			} );
 
-			upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher );
-			upcastElementToAttribute( { view: 'strong', model: 'bold' } )( data.upcastDispatcher );
+			upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );
+			upcastHelpers.elementToAttribute( { view: 'strong', model: 'bold' } );
 
 			const output = data.parse( '<p>foo<strong>bar</strong></p>' );
 
@@ -119,7 +114,7 @@ describe( 'DataController', () => {
 		beforeEach( () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
-			upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher );
+			upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );
 		} );
 
 		it( 'should convert content of an element #1', () => {
@@ -285,7 +280,7 @@ describe( 'DataController', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo</paragraph>' );
 
-			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
 
 			expect( data.get() ).to.equal( '<p>foo</p>' );
 		} );
@@ -294,7 +289,7 @@ describe( 'DataController', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph></paragraph>' );
 
-			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
 
 			expect( data.get() ).to.equal( '<p>&nbsp;</p>' );
 		} );
@@ -303,7 +298,7 @@ describe( 'DataController', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
 
-			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
 
 			expect( data.get() ).to.equal( '<p>foo</p><p>bar</p>' );
 		} );
@@ -319,7 +314,7 @@ describe( 'DataController', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
-			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
 
 			expect( data.get() ).to.equal( '<p>foobar</p>' );
 		} );
@@ -328,8 +323,8 @@ describe( 'DataController', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
-			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
-			downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
+			downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
 
 			expect( data.get() ).to.equal( '<p>foo<strong>bar</strong></p>' );
 		} );
@@ -341,8 +336,8 @@ describe( 'DataController', () => {
 			setData( model, '<paragraph>foo</paragraph>', { rootName: 'main' } );
 			setData( model, 'Bar', { rootName: 'title' } );
 
-			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
-			downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
+			downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
 
 			expect( data.get() ).to.equal( '<p>foo</p>' );
 			expect( data.get( 'main' ) ).to.equal( '<p>foo</p>' );
@@ -358,7 +353,7 @@ describe( 'DataController', () => {
 			schema.extend( '$block', { allowIn: 'div' } );
 			schema.extend( 'div', { allowIn: '$root' } );
 
-			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
 		} );
 
 		it( 'should stringify a content of an element', () => {
@@ -382,7 +377,7 @@ describe( 'DataController', () => {
 			schema.extend( '$block', { allowIn: 'div' } );
 			schema.extend( 'div', { allowIn: '$root' } );
 
-			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
 		} );
 
 		it( 'should convert a content of an element', () => {
@@ -403,7 +398,7 @@ describe( 'DataController', () => {
 			const modelElement = parseModel( '<div><paragraph>foobar</paragraph></div>', schema );
 			const modelRoot = model.document.getRoot();
 
-			downcastMarkerToHighlight( { model: 'marker:a', view: { classes: 'a' } } )( data.downcastDispatcher );
+			downcastHelpers.markerToHighlight( { model: 'marker:a', view: { classes: 'a' } } );
 
 			model.change( writer => {
 				writer.insert( modelElement, modelRoot, 0 );
@@ -421,8 +416,8 @@ describe( 'DataController', () => {
 			const modelElement = parseModel( '<div><paragraph>foo</paragraph><paragraph>bar</paragraph></div>', schema );
 			const modelRoot = model.document.getRoot();
 
-			downcastMarkerToHighlight( { model: 'marker:a', view: { classes: 'a' } } )( data.downcastDispatcher );
-			downcastMarkerToHighlight( { model: 'marker:b', view: { classes: 'b' } } )( data.downcastDispatcher );
+			downcastHelpers.markerToHighlight( { model: 'marker:a', view: { classes: 'a' } } );
+			downcastHelpers.markerToHighlight( { model: 'marker:b', view: { classes: 'b' } } );
 
 			const modelP1 = modelElement.getChild( 0 );
 			const modelP2 = modelElement.getChild( 1 );

+ 6 - 5
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -14,8 +14,7 @@ import View from '../../src/view/view';
 import Mapper from '../../src/conversion/mapper';
 import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
 
-import { downcastElementToElement, downcastMarkerToHighlight } from '../../src/conversion/downcast-converters';
-
+import DowncastHelpers from '../../src/conversion/downcasthelpers';
 import Model from '../../src/model/model';
 import ModelPosition from '../../src/model/position';
 import ModelRange from '../../src/model/range';
@@ -91,9 +90,11 @@ describe( 'EditingController', () => {
 			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			model.schema.register( 'div', { inheritAllFrom: '$block' } );
 
-			downcastElementToElement( { model: 'paragraph', view: 'p' } )( editing.downcastDispatcher );
-			downcastElementToElement( { model: 'div', view: 'div' } )( editing.downcastDispatcher );
-			downcastMarkerToHighlight( { model: 'marker', view: {} } )( editing.downcastDispatcher );
+			const downcastHelpers = new DowncastHelpers( editing.downcastDispatcher );
+
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
+			downcastHelpers.elementToElement( { model: 'div', view: 'div' } );
+			downcastHelpers.markerToHighlight( { model: 'marker', view: {} } );
 
 			// Note: The below code is highly overcomplicated due to #455.
 			model.change( writer => {

+ 57 - 13
packages/ckeditor5-engine/tests/conversion/conversion.js

@@ -3,22 +3,23 @@
  * For licensing, see LICENSE.md.
  */
 
-import Conversion from '../../src/conversion/conversion';
+import Conversion, { ConversionHelpers } from '../../src/conversion/conversion';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
 
-import { convertText, convertToModelFragment } from '../../src/conversion/upcast-converters';
+import UpcastHelpers, { convertText, convertToModelFragment } from '../../src/conversion/upcasthelpers';
+import DowncastHelpers from '../../src/conversion/downcasthelpers';
 
 import EditingController from '../../src/controller/editingcontroller';
 
 import Model from '../../src/model/model';
 
-import { stringify as viewStringify, parse as viewParse } from '../../src/dev-utils/view';
+import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view';
 import { stringify as modelStringify } from '../../src/dev-utils/model';
 
 describe( 'Conversion', () => {
-	let conversion, dispA, dispB;
+	let conversion, dispA, dispB, dispC;
 
 	beforeEach( () => {
 		conversion = new Conversion();
@@ -26,10 +27,12 @@ describe( 'Conversion', () => {
 		// Placeholders. Will be used only to see if their were given as attribute for a spy function.
 		dispA = Symbol( 'dispA' );
 		dispB = Symbol( 'dispB' );
+		dispC = Symbol( 'dispC' );
 
-		conversion.register( 'ab', [ dispA, dispB ] );
-		conversion.register( 'a', [ dispA ] );
-		conversion.register( 'b', [ dispB ] );
+		conversion.register( 'ab', new UpcastHelpers( [ dispA, dispB ] ) );
+		conversion.register( 'a', new UpcastHelpers( dispA ) );
+		conversion.register( 'b', new UpcastHelpers( dispB ) );
+		conversion.register( 'c', new DowncastHelpers( dispC ) );
 	} );
 
 	describe( 'register()', () => {
@@ -41,10 +44,8 @@ describe( 'Conversion', () => {
 	} );
 
 	describe( 'for()', () => {
-		it( 'should return object with .add() method', () => {
-			const forResult = conversion.for( 'ab' );
-
-			expect( forResult.add ).to.be.instanceof( Function );
+		it( 'should return ConversionHelpers', () => {
+			expect( conversion.for( 'ab' ) ).to.be.instanceof( ConversionHelpers );
 		} );
 
 		it( 'should throw if non-existing group name has been used', () => {
@@ -52,6 +53,13 @@ describe( 'Conversion', () => {
 				conversion.for( 'foo' );
 			} ).to.throw( CKEditorError, /conversion-for-unknown-group/ );
 		} );
+
+		it( 'should return proper helpers for group', () => {
+			expect( conversion.for( 'ab' ) ).to.be.instanceof( UpcastHelpers );
+			expect( conversion.for( 'a' ) ).to.be.instanceof( UpcastHelpers );
+			expect( conversion.for( 'b' ) ).to.be.instanceof( UpcastHelpers );
+			expect( conversion.for( 'c' ) ).to.be.instanceof( DowncastHelpers );
+		} );
 	} );
 
 	describe( 'add()', () => {
@@ -100,6 +108,7 @@ describe( 'Conversion', () => {
 			schema = model.schema;
 
 			schema.extend( '$text', {
+				allowIn: '$root',
 				allowAttributes: [ 'bold' ]
 			} );
 
@@ -113,8 +122,8 @@ describe( 'Conversion', () => {
 			viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
 
 			conversion = new Conversion();
-			conversion.register( 'upcast', [ viewDispatcher ] );
-			conversion.register( 'downcast', [ controller.downcastDispatcher ] );
+			conversion.register( 'upcast', new UpcastHelpers( [ viewDispatcher ] ) );
+			conversion.register( 'downcast', new DowncastHelpers( controller.downcastDispatcher ) );
 		} );
 
 		describe( 'elementToElement', () => {
@@ -652,3 +661,38 @@ describe( 'Conversion', () => {
 		}
 	} );
 } );
+
+describe( 'ConversionHelpers', () => {
+	describe( 'add()', () => {
+		const dispA = Symbol( 'dispA' );
+		const dispB = Symbol( 'dispB' );
+
+		it( 'should call a helper for one defined dispatcher', () => {
+			const spy = sinon.spy();
+			const helpers = new ConversionHelpers( dispA );
+
+			helpers.add( spy );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWithExactly( spy, dispA );
+		} );
+
+		it( 'should call helper for all defined dispatcherers', () => {
+			const spy = sinon.spy();
+			const helpers = new ConversionHelpers( [ dispA, dispB ] );
+
+			helpers.add( spy );
+
+			sinon.assert.calledTwice( spy );
+			sinon.assert.calledWithExactly( spy, dispA );
+			sinon.assert.calledWithExactly( spy, dispB );
+		} );
+
+		it( 'should be chainable', () => {
+			const spy = sinon.spy();
+			const helpers = new ConversionHelpers( dispA );
+
+			expect( helpers.add( spy ) ).to.equal( helpers );
+		} );
+	} );
+} );

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

@@ -16,21 +16,14 @@ import {
 	clearAttributes,
 } from '../../src/conversion/downcast-selection-converters';
 
-import {
-	insertElement,
-	insertText,
-	wrap,
-	highlightElement,
-	highlightText,
-	removeHighlight
-} from '../../src/conversion/downcast-converters';
+import DowncastHelpers, { insertText, wrap } from '../../src/conversion/downcasthelpers';
 
 import createViewRoot from '../view/_utils/createroot';
 import { stringify as stringifyView } from '../../src/dev-utils/view';
 import { setData as setModelData } from '../../src/dev-utils/model';
 
 describe( 'downcast-selection-converters', () => {
-	let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor;
+	let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, downcastHelpers;
 
 	beforeEach( () => {
 		model = new Model();
@@ -48,8 +41,6 @@ describe( 'downcast-selection-converters', () => {
 		mapper = new Mapper();
 		mapper.bindElements( modelRoot, viewRoot );
 
-		highlightDescriptor = { classes: 'marker', priority: 1 };
-
 		dispatcher = new DowncastDispatcher( { mapper, viewSelection } );
 
 		dispatcher.on( 'insert:$text', insertText() );
@@ -57,9 +48,8 @@ describe( 'downcast-selection-converters', () => {
 		const strongCreator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' );
 		dispatcher.on( 'attribute:bold', wrap( strongCreator ) );
 
-		dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
-		dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
-		dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) );
+		downcastHelpers = new DowncastHelpers( dispatcher );
+		downcastHelpers.markerToHighlight( { model: 'marker', view: { classes: 'marker' }, converterPriority: 1 } );
 
 		// Default selection converters.
 		dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
@@ -241,9 +231,10 @@ describe( 'downcast-selection-converters', () => {
 			} );
 
 			it( 'in marker - using highlight descriptor creator', () => {
-				dispatcher.on( 'addMarker:marker2', highlightText(
-					data => ( { classes: data.markerName } )
-				) );
+				downcastHelpers.markerToHighlight( {
+					model: 'marker2',
+					view: data => ( { classes: data.markerName } )
+				} );
 
 				setModelData( model, 'foobar' );
 
@@ -269,7 +260,10 @@ describe( 'downcast-selection-converters', () => {
 			} );
 
 			it( 'should do nothing if creator return null', () => {
-				dispatcher.on( 'addMarker:marker3', highlightText( () => null ) );
+				downcastHelpers.markerToHighlight( {
+					model: 'marker3',
+					view: () => null
+				} );
 
 				setModelData( model, 'foobar' );
 
@@ -500,12 +494,12 @@ describe( 'downcast-selection-converters', () => {
 			model.schema.extend( 'td', { allowIn: 'tr' } );
 			model.schema.extend( '$text', { allowIn: 'td' } );
 
+			const downcastHelpers = new DowncastHelpers( dispatcher );
+
 			// "Universal" converter to convert table structure.
-			const containerCreator = ( modelElement, viewWriter ) => viewWriter.createContainerElement( modelElement.name );
-			const tableConverter = insertElement( containerCreator );
-			dispatcher.on( 'insert:table', tableConverter );
-			dispatcher.on( 'insert:tr', tableConverter );
-			dispatcher.on( 'insert:td', tableConverter );
+			downcastHelpers.elementToElement( { model: 'table', view: 'table' } );
+			downcastHelpers.elementToElement( { model: 'tr', view: 'tr' } );
+			downcastHelpers.elementToElement( { model: 'td', view: 'td' } );
 
 			// Special converter for table cells.
 			dispatcher.on( 'selection', ( evt, data, conversionApi ) => {

Разлика између датотеке није приказан због своје велике величине
+ 864 - 181
packages/ckeditor5-engine/tests/conversion/downcasthelpers.js


+ 76 - 129
packages/ckeditor5-engine/tests/conversion/upcast-converters.js → packages/ckeditor5-engine/tests/conversion/upcasthelpers.js

@@ -19,15 +19,12 @@ import ModelText from '../../src/model/text';
 import ModelRange from '../../src/model/range';
 import ModelPosition from '../../src/model/position';
 
-import {
-	upcastElementToElement, upcastElementToAttribute, upcastAttributeToAttribute, upcastElementToMarker,
-	convertToModelFragment, convertText
-} from '../../src/conversion/upcast-converters';
+import UpcastHelpers, { convertToModelFragment, convertText } from '../../src/conversion/upcasthelpers';
 
 import { stringify } from '../../src/dev-utils/model';
 
-describe( 'upcast-helpers', () => {
-	let upcastDispatcher, model, schema, conversion;
+describe( 'UpcastHelpers', () => {
+	let upcastDispatcher, model, schema, conversion, upcastHelpers;
 
 	beforeEach( () => {
 		model = new Model();
@@ -49,14 +46,17 @@ describe( 'upcast-helpers', () => {
 		upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
 
 		conversion = new Conversion();
-		conversion.register( 'upcast', [ upcastDispatcher ] );
+		upcastHelpers = new UpcastHelpers( upcastDispatcher );
+		conversion.register( 'upcast', upcastHelpers );
 	} );
 
-	describe( 'upcastElementToElement', () => {
-		it( 'config.view is a string', () => {
-			const helper = upcastElementToElement( { view: 'p', model: 'paragraph' } );
+	describe( '.elementToElement()', () => {
+		it( 'should be chainable', () => {
+			expect( upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } ) ).to.equal( upcastHelpers );
+		} );
 
-			conversion.for( 'upcast' ).add( helper );
+		it( 'config.view is a string', () => {
+			upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );
 
 			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
 		} );
@@ -66,10 +66,8 @@ describe( 'upcast-helpers', () => {
 				inheritAllFrom: '$block'
 			} );
 
-			const helperA = upcastElementToElement( { view: 'p', model: 'p' } );
-			const helperB = upcastElementToElement( { view: 'p', model: 'paragraph', converterPriority: 'high' } );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+			upcastHelpers.elementToElement( { view: 'p', model: 'p' } );
+			upcastHelpers.elementToElement( { view: 'p', model: 'paragraph', converterPriority: 'high' } );
 
 			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
 		} );
@@ -79,16 +77,14 @@ describe( 'upcast-helpers', () => {
 				inheritAllFrom: '$block'
 			} );
 
-			const helperFancy = upcastElementToElement( {
+			upcastHelpers.elementToElement( {
 				view: {
 					name: 'p',
 					classes: 'fancy'
 				},
-				model: 'fancyParagraph',
+				model: 'fancyParagraph'
 			} );
 
-			conversion.for( 'upcast' ).add( helperFancy );
-
 			expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<fancyParagraph></fancyParagraph>' );
 			expectResult( new ViewContainerElement( 'p' ), '' );
 		} );
@@ -99,7 +95,7 @@ describe( 'upcast-helpers', () => {
 				allowAttributes: [ 'level' ]
 			} );
 
-			const helper = upcastElementToElement( {
+			upcastHelpers.elementToElement( {
 				view: {
 					name: 'p',
 					classes: 'heading'
@@ -109,24 +105,18 @@ describe( 'upcast-helpers', () => {
 				}
 			} );
 
-			conversion.for( 'upcast' ).add( helper );
-
 			expectResult( new ViewContainerElement( 'p', { class: 'heading', 'data-level': 2 } ), '<heading level="2"></heading>' );
 			expectResult( new ViewContainerElement( 'p', { 'data-level': 2 } ), '' );
 		} );
 
 		it( 'should fire conversion of the element children', () => {
-			const helper = upcastElementToElement( { view: 'p', model: 'paragraph' } );
-
-			conversion.for( 'upcast' ).add( helper );
+			upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );
 
 			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 );
+			upcastHelpers.elementToElement( { view: 'h2', model: 'heading' } );
 
 			expectResult( new ViewContainerElement( 'h2' ), '' );
 		} );
@@ -136,10 +126,8 @@ describe( 'upcast-helpers', () => {
 				inheritAllFrom: '$block'
 			} );
 
-			const helperParagraph = upcastElementToElement( { view: 'p', model: 'paragraph' } );
-			const helperHeading = upcastElementToElement( { view: 'h2', model: 'heading' } );
-
-			conversion.for( 'upcast' ).add( helperParagraph ).add( helperHeading );
+			upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );
+			upcastHelpers.elementToElement( { view: 'h2', model: 'heading' } );
 
 			expectResult(
 				new ViewContainerElement( 'p', null, [
@@ -152,20 +140,20 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		it( 'should not do anything if returned model element is null', () => {
-			const helperA = upcastElementToElement( { view: 'p', model: 'paragraph' } );
-			const helperB = upcastElementToElement( { view: 'p', model: () => null, converterPriority: 'high' } );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+			upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );
+			upcastHelpers.elementToElement( { view: 'p', model: () => null, converterPriority: 'high' } );
 
 			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
 		} );
 	} );
 
-	describe( 'upcastElementToAttribute', () => {
-		it( 'config.view is string', () => {
-			const helper = upcastElementToAttribute( { view: 'strong', model: 'bold' } );
+	describe( '.elementToAttribute()', () => {
+		it( 'should be chainable', () => {
+			expect( upcastHelpers.elementToAttribute( { view: 'strong', model: 'bold' } ) ).to.equal( upcastHelpers );
+		} );
 
-			conversion.for( 'upcast' ).add( helper );
+		it( 'config.view is string', () => {
+			upcastHelpers.elementToAttribute( { view: 'strong', model: 'bold' } );
 
 			expectResult(
 				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
@@ -174,10 +162,8 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		it( 'can be overwritten using converterPriority', () => {
-			const helperA = upcastElementToAttribute( { view: 'strong', model: 'strong' } );
-			const helperB = upcastElementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+			upcastHelpers.elementToAttribute( { view: 'strong', model: 'strong' } );
+			upcastHelpers.elementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } );
 
 			expectResult(
 				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
@@ -186,7 +172,7 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		it( 'config.view is an object', () => {
-			const helper = upcastElementToAttribute( {
+			upcastHelpers.elementToAttribute( {
 				view: {
 					name: 'span',
 					classes: 'bold'
@@ -194,8 +180,6 @@ describe( 'upcast-helpers', () => {
 				model: 'bold'
 			} );
 
-			conversion.for( 'upcast' ).add( helper );
-
 			expectResult(
 				new ViewAttributeElement( 'span', { class: 'bold' }, new ViewText( 'foo' ) ),
 				'<$text bold="true">foo</$text>'
@@ -209,7 +193,7 @@ describe( 'upcast-helpers', () => {
 				allowAttributes: [ 'styled' ]
 			} );
 
-			const helper = upcastElementToAttribute( {
+			upcastHelpers.elementToAttribute( {
 				view: {
 					name: 'span',
 					classes: [ 'styled', 'styled-dark' ]
@@ -220,8 +204,6 @@ describe( 'upcast-helpers', () => {
 				}
 			} );
 
-			conversion.for( 'upcast' ).add( helper );
-
 			expectResult(
 				new ViewAttributeElement( 'span', { class: 'styled styled-dark' }, new ViewText( 'foo' ) ),
 				'<$text styled="dark">foo</$text>'
@@ -235,7 +217,7 @@ describe( 'upcast-helpers', () => {
 				allowAttributes: [ 'fontSize' ]
 			} );
 
-			const helper = upcastElementToAttribute( {
+			upcastHelpers.elementToAttribute( {
 				view: {
 					name: 'span',
 					styles: {
@@ -259,8 +241,6 @@ describe( 'upcast-helpers', () => {
 				}
 			} );
 
-			conversion.for( 'upcast' ).add( helper );
-
 			expectResult(
 				new ViewAttributeElement( 'span', { style: 'font-size:9px' }, new ViewText( 'foo' ) ),
 				'<$text fontSize="small">foo</$text>'
@@ -278,9 +258,7 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		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 );
+			upcastHelpers.elementToAttribute( { view: 'em', model: 'italic' } );
 
 			expectResult(
 				new ViewAttributeElement( 'em', null, new ViewText( 'foo' ) ),
@@ -289,8 +267,8 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		it( 'should not do anything if returned model attribute is null', () => {
-			const helperA = upcastElementToAttribute( { view: 'strong', model: 'bold' } );
-			const helperB = upcastElementToAttribute( {
+			upcastHelpers.elementToAttribute( { view: 'strong', model: 'bold' } );
+			upcastHelpers.elementToAttribute( {
 				view: 'strong',
 				model: {
 					key: 'bold',
@@ -299,8 +277,6 @@ describe( 'upcast-helpers', () => {
 				converterPriority: 'high'
 			} );
 
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
-
 			expectResult(
 				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
 				'<$text bold="true">foo</$text>'
@@ -308,18 +284,16 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		it( 'should allow two converters to convert attributes on the same element', () => {
-			const helperA = upcastElementToAttribute( {
+			upcastHelpers.elementToAttribute( {
 				model: 'attribA',
 				view: { name: 'span', classes: 'attrib-a' }
 			} );
 
-			const helperB = upcastElementToAttribute( {
+			upcastHelpers.elementToAttribute( {
 				model: 'attribB',
 				view: { name: 'span', styles: { color: 'attrib-b' } }
 			} );
 
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
-
 			expectResult(
 				new ViewAttributeElement( 'span', { class: 'attrib-a', style: 'color:attrib-b;' }, new ViewText( 'foo' ) ),
 				'<$text attribA="true" attribB="true">foo</$text>'
@@ -327,23 +301,21 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		it( 'should consume element only when only is name specified', () => {
-			const helperBold = upcastElementToAttribute( {
+			upcastHelpers.elementToAttribute( {
 				model: 'bold',
 				view: { name: 'strong' }
 			} );
 
-			const helperA = upcastElementToAttribute( {
+			upcastHelpers.elementToAttribute( {
 				model: 'attribA',
 				view: { name: 'strong' }
 			} );
 
-			const helperB = upcastElementToAttribute( {
+			upcastHelpers.elementToAttribute( {
 				model: 'attribB',
 				view: { name: 'strong', classes: 'foo' }
 			} );
 
-			conversion.for( 'upcast' ).add( helperBold ).add( helperA ).add( helperB );
-
 			expectResult(
 				new ViewAttributeElement( 'strong', { class: 'foo' }, new ViewText( 'foo' ) ),
 				'<$text attribB="true" bold="true">foo</$text>'
@@ -352,14 +324,12 @@ describe( 'upcast-helpers', () => {
 
 		// #1443.
 		it( 'should set attributes on the element\'s children', () => {
-			const helperBold = upcastElementToAttribute( {
+			upcastHelpers.elementToAttribute( {
 				model: 'bold',
 				view: { name: 'strong' }
 			} );
 
-			const helperP = upcastElementToElement( { view: 'p', model: 'paragraph' } );
-
-			conversion.for( 'upcast' ).add( helperP ).add( helperBold );
+			upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );
 
 			expectResult(
 				new ViewAttributeElement(
@@ -372,23 +342,25 @@ describe( 'upcast-helpers', () => {
 		} );
 	} );
 
-	describe( 'upcastAttributeToAttribute', () => {
+	describe( '.attributeToAttribute()', () => {
 		beforeEach( () => {
-			conversion.for( 'upcast' ).add( upcastElementToElement( { view: 'img', model: 'image' } ) );
+			upcastHelpers.elementToElement( { view: 'img', model: 'image' } );
 
 			schema.register( 'image', {
 				inheritAllFrom: '$block'
 			} );
 		} );
 
+		it( 'should be chainable', () => {
+			expect( upcastHelpers.attributeToAttribute( { view: 'src', model: 'source' } ) ).to.equal( upcastHelpers );
+		} );
+
 		it( 'config.view is a string', () => {
 			schema.extend( 'image', {
 				allowAttributes: [ 'source' ]
 			} );
 
-			const helper = upcastAttributeToAttribute( { view: 'src', model: 'source' } );
-
-			conversion.for( 'upcast' ).add( helper );
+			upcastHelpers.attributeToAttribute( { view: 'src', model: 'source' } );
 
 			expectResult(
 				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
@@ -401,9 +373,7 @@ describe( 'upcast-helpers', () => {
 				allowAttributes: [ 'source' ]
 			} );
 
-			const helper = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } );
-
-			conversion.for( 'upcast' ).add( helper );
+			upcastHelpers.attributeToAttribute( { view: { key: 'src' }, model: 'source' } );
 
 			expectResult(
 				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
@@ -416,9 +386,7 @@ describe( 'upcast-helpers', () => {
 				allowAttributes: [ 'source' ]
 			} );
 
-			const helper = upcastAttributeToAttribute( { view: { name: 'img', key: 'src' }, model: { name: 'image', key: 'source' } } );
-
-			conversion.for( 'upcast' ).add( helper );
+			upcastHelpers.attributeToAttribute( { view: { name: 'img', key: 'src' }, model: { name: 'image', key: 'source' } } );
 
 			expectResult(
 				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
@@ -431,10 +399,8 @@ describe( 'upcast-helpers', () => {
 				allowAttributes: [ 'src', 'source' ]
 			} );
 
-			const helperA = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'src' } );
-			const helperB = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+			upcastHelpers.attributeToAttribute( { view: { key: 'src' }, model: 'src' } );
+			upcastHelpers.attributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } );
 
 			expectResult(
 				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
@@ -447,7 +413,7 @@ describe( 'upcast-helpers', () => {
 				allowAttributes: [ 'styled' ]
 			} );
 
-			const helper = upcastAttributeToAttribute( {
+			upcastHelpers.attributeToAttribute( {
 				view: {
 					key: 'data-style',
 					value: /[\s\S]*/
@@ -455,8 +421,6 @@ describe( 'upcast-helpers', () => {
 				model: 'styled'
 			} );
 
-			conversion.for( 'upcast' ).add( helper );
-
 			expectResult(
 				new ViewAttributeElement( 'img', { 'data-style': 'dark' } ),
 				'<image styled="dark"></image>'
@@ -468,7 +432,7 @@ describe( 'upcast-helpers', () => {
 				allowAttributes: [ 'styled' ]
 			} );
 
-			const helper = upcastAttributeToAttribute( {
+			upcastHelpers.attributeToAttribute( {
 				view: {
 					name: 'img',
 					key: 'class',
@@ -480,9 +444,7 @@ describe( 'upcast-helpers', () => {
 				}
 			} );
 
-			conversion.for( 'upcast' )
-				.add( helper )
-				.add( upcastElementToElement( { view: 'p', model: 'paragraph' } ) );
+			upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );
 
 			expectResult(
 				new ViewContainerElement( 'img', { class: 'styled-dark' } ),
@@ -505,7 +467,7 @@ describe( 'upcast-helpers', () => {
 				allowAttributes: [ 'styled' ]
 			} );
 
-			const helper = upcastAttributeToAttribute( {
+			upcastHelpers.attributeToAttribute( {
 				view: {
 					key: 'class',
 					value: /styled-[\S]+/
@@ -521,8 +483,6 @@ describe( 'upcast-helpers', () => {
 				}
 			} );
 
-			conversion.for( 'upcast' ).add( helper );
-
 			expectResult(
 				new ViewAttributeElement( 'img', { 'class': 'styled-dark' } ),
 				'<image styled="dark"></image>'
@@ -530,9 +490,7 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		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 );
+			upcastHelpers.attributeToAttribute( { view: 'src', model: 'source' } );
 
 			expectResult(
 				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
@@ -545,7 +503,7 @@ describe( 'upcast-helpers', () => {
 				allowAttributes: [ 'styled' ]
 			} );
 
-			const helperA = upcastAttributeToAttribute( {
+			upcastHelpers.attributeToAttribute( {
 				view: {
 					key: 'class',
 					value: 'styled'
@@ -556,7 +514,7 @@ describe( 'upcast-helpers', () => {
 				}
 			} );
 
-			const helperB = upcastAttributeToAttribute( {
+			upcastHelpers.attributeToAttribute( {
 				view: {
 					key: 'class',
 					value: 'styled'
@@ -567,8 +525,6 @@ describe( 'upcast-helpers', () => {
 				}
 			} );
 
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
-
 			expectResult(
 				new ViewAttributeElement( 'img', { class: 'styled' } ),
 				'<image styled="true"></image>'
@@ -584,13 +540,10 @@ describe( 'upcast-helpers', () => {
 				allowAttributes: [ 'border', 'shade' ]
 			} );
 
-			conversion.for( 'upcast' ).add( upcastElementToElement( { view: 'div', model: 'div' } ) );
-
-			const shadeHelper = upcastAttributeToAttribute( { view: { key: 'class', value: 'shade' }, model: 'shade' } );
-			const borderHelper = upcastAttributeToAttribute( { view: { key: 'class', value: 'border' }, model: 'border' } );
+			upcastHelpers.elementToElement( { view: 'div', model: 'div' } );
 
-			conversion.for( 'upcast' ).add( shadeHelper );
-			conversion.for( 'upcast' ).add( borderHelper );
+			upcastHelpers.attributeToAttribute( { view: { key: 'class', value: 'shade' }, model: 'shade' } );
+			upcastHelpers.attributeToAttribute( { view: { key: 'class', value: 'border' }, model: 'border' } );
 
 			expectResult(
 				new ViewContainerElement(
@@ -603,11 +556,13 @@ describe( 'upcast-helpers', () => {
 		} );
 	} );
 
-	describe( 'upcastElementToMarker', () => {
-		it( 'config.view is a string', () => {
-			const helper = upcastElementToMarker( { view: 'marker-search', model: 'search' } );
+	describe( '.elementToMarker()', () => {
+		it( 'should be chainable', () => {
+			expect( upcastHelpers.elementToMarker( { view: 'marker-search', model: 'search' } ) ).to.equal( upcastHelpers );
+		} );
 
-			conversion.for( 'upcast' ).add( helper );
+		it( 'config.view is a string', () => {
+			upcastHelpers.elementToMarker( { view: 'marker-search', model: 'search' } );
 
 			const frag = new ViewDocumentFragment( [
 				new ViewText( 'fo' ),
@@ -623,10 +578,8 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		it( 'can be overwritten using converterPriority', () => {
-			const helperA = upcastElementToMarker( { view: 'marker-search', model: 'search-result' } );
-			const helperB = upcastElementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } );
-
-			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+			upcastHelpers.elementToMarker( { view: 'marker-search', model: 'search-result' } );
+			upcastHelpers.elementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } );
 
 			const frag = new ViewDocumentFragment( [
 				new ViewText( 'fo' ),
@@ -642,7 +595,7 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		it( 'config.view is an object', () => {
-			const helper = upcastElementToMarker( {
+			upcastHelpers.elementToMarker( {
 				view: {
 					name: 'span',
 					'data-marker': 'search'
@@ -650,8 +603,6 @@ describe( 'upcast-helpers', () => {
 				model: 'search'
 			} );
 
-			conversion.for( 'upcast' ).add( helper );
-
 			const frag = new ViewDocumentFragment( [
 				new ViewText( 'f' ),
 				new ViewUIElement( 'span', { 'data-marker': 'search' } ),
@@ -666,13 +617,11 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		it( 'config.model is a function', () => {
-			const helper = upcastElementToMarker( {
+			upcastHelpers.elementToMarker( {
 				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 } ),
@@ -687,11 +636,9 @@ describe( 'upcast-helpers', () => {
 		} );
 
 		it( 'marker is in a block element', () => {
-			conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'paragraph', view: 'p' } ) );
-
-			const helper = upcastElementToMarker( { view: 'marker-search', model: 'search' } );
+			upcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
 
-			conversion.for( 'upcast' ).add( helper );
+			upcastHelpers.elementToMarker( { view: 'marker-search', model: 'search' } );
 
 			const element = new ViewContainerElement( 'p', null, [
 				new ViewText( 'fo' ),

+ 22 - 0
packages/ckeditor5-engine/tests/dev-utils/model.js

@@ -79,6 +79,28 @@ describe( 'model test utils', () => {
 				getData( { invalid: 'document' } );
 			} ).to.throw( TypeError, 'Model needs to be an instance of module:engine/model/model~Model.' );
 		} );
+
+		describe( 'markers', () => {
+			it( 'should stringify collapsed marker', () => {
+				setData( model, '<paragraph>bar</paragraph>' );
+
+				model.markers._set( 'foo', new Range( Position._createAt( document.getRoot(), 0 ) ) );
+
+				expect( getData( model, { convertMarkers: true, withoutSelection: true } ) )
+					.to.equal( '<foo:start></foo:start><paragraph>bar</paragraph>' );
+			} );
+
+			it( 'should stringify non-collapsed marker', () => {
+				setData( model, '<paragraph>bar</paragraph>' );
+
+				const markerRange = new Range( Position._createAt( document.getRoot(), 0 ), Position._createAt( document.getRoot(), 1 ) );
+
+				model.markers._set( 'foo', markerRange );
+
+				expect( getData( model, { convertMarkers: true, withoutSelection: true } ) )
+					.to.equal( '<foo:start></foo:start><paragraph>bar</paragraph><foo:end></foo:end>' );
+			} );
+		} );
 	} );
 
 	describe( 'setData', () => {

+ 8 - 18
packages/ckeditor5-engine/tests/manual/highlight.js

@@ -5,15 +5,6 @@
 
 /* global console, window, document */
 
-import {
-	upcastElementToElement,
-} from '../../src/conversion/upcast-converters';
-
-import {
-	downcastElementToElement,
-	downcastMarkerToHighlight
-} from '../../src/conversion/downcast-converters';
-
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
@@ -44,7 +35,7 @@ class FancyWidget extends Plugin {
 		} );
 		schema.extend( 'fancywidget', { allowIn: '$root' } );
 
-		conversion.for( 'editingDowncast' ).add( downcastElementToElement( {
+		conversion.for( 'editingDowncast' ).elementToElement( {
 			model: 'fancywidget',
 			view: ( modelItem, viewWriter ) => {
 				const widgetElement = viewWriter.createContainerElement( 'figure', { class: 'fancy-widget' } );
@@ -52,13 +43,12 @@ class FancyWidget extends Plugin {
 
 				return toWidget( widgetElement, viewWriter );
 			}
-		} ) );
+		} );
 
-		conversion.for( 'upcast' )
-			.add( upcastElementToElement( {
-				view: 'figure',
-				model: 'fancywidget'
-			} ) );
+		conversion.for( 'upcast' ).elementToElement( {
+			view: 'figure',
+			model: 'fancywidget'
+		} );
 	}
 }
 
@@ -69,12 +59,12 @@ ClassicEditor.create( global.document.querySelector( '#editor' ), {
 	.then( editor => {
 		window.editor = editor;
 
-		editor.conversion.for( 'editingDowncast' ).add( downcastMarkerToHighlight( {
+		editor.conversion.for( 'editingDowncast' ).markerToHighlight( {
 			model: 'marker',
 			view: data => ( {
 				classes: 'highlight-' + data.markerName.split( ':' )[ 1 ]
 			} )
-		} ) );
+		} );
 
 		document.getElementById( 'add-marker-yellow' ).addEventListener( 'mousedown', evt => {
 			addMarker( editor, 'yellow' );

+ 4 - 8
packages/ckeditor5-engine/tests/manual/markers.js

@@ -15,10 +15,6 @@ import List from '@ckeditor/ckeditor5-list/src/list';
 import Heading from '@ckeditor/ckeditor5-heading/src/heading';
 import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 
-import {
-	downcastMarkerToHighlight
-} from '../../src/conversion/downcast-converters';
-
 import Position from '../../src/model/position';
 import Range from '../../src/model/range';
 
@@ -35,7 +31,7 @@ ClassicEditor
 		window.editor = editor;
 		model = editor.model;
 
-		editor.conversion.for( 'editingDowncast' ).add( downcastMarkerToHighlight( {
+		editor.conversion.for( 'editingDowncast' ).markerToHighlight( {
 			model: 'highlight',
 			view: data => {
 				const color = data.markerName.split( ':' )[ 1 ];
@@ -45,9 +41,9 @@ ClassicEditor
 					priority: 1
 				};
 			}
-		} ) );
+		} );
 
-		editor.conversion.for( 'dataDowncast' ).add( downcastMarkerToHighlight( {
+		editor.conversion.for( 'dataDowncast' ).markerToHighlight( {
 			model: 'highlight',
 			view: data => {
 				const color = data.markerName.split( ':' )[ 1 ];
@@ -57,7 +53,7 @@ ClassicEditor
 					priority: 1
 				};
 			}
-		} ) );
+		} );
 
 		window.document.getElementById( 'add-yellow' ).addEventListener( 'mousedown', e => {
 			e.preventDefault();

+ 8 - 16
packages/ckeditor5-engine/tests/manual/nestededitable.js

@@ -5,14 +5,6 @@
 
 /* global console */
 
-import {
-	upcastElementToElement
-} from '../../src/conversion/upcast-converters';
-
-import {
-	downcastElementToElement
-} from '../../src/conversion/downcast-converters';
-
 import { getData } from '../../src/dev-utils/model';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
@@ -41,7 +33,7 @@ class NestedEditable extends Plugin {
 			allowIn: [ 'figure', 'figcaption' ]
 		} );
 
-		editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
+		editor.conversion.for( 'downcast' ).elementToElement( {
 			model: 'figure',
 			view: {
 				name: 'figure',
@@ -49,14 +41,14 @@ class NestedEditable extends Plugin {
 					contenteditable: 'false'
 				}
 			}
-		} ) );
+		} );
 
-		editor.conversion.for( 'upcast' ).add( upcastElementToElement( {
+		editor.conversion.for( 'upcast' ).elementToElement( {
 			model: 'figure',
 			view: 'figure'
-		} ) );
+		} );
 
-		editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
+		editor.conversion.for( 'downcast' ).elementToElement( {
 			model: 'figcaption',
 			view: ( modelItem, viewWriter ) => {
 				const element = viewWriter.createEditableElement( 'figcaption', { contenteditable: 'true' } );
@@ -71,12 +63,12 @@ class NestedEditable extends Plugin {
 
 				return element;
 			}
-		} ) );
+		} );
 
-		editor.conversion.for( 'upcast' ).add( upcastElementToElement( {
+		editor.conversion.for( 'upcast' ).elementToElement( {
 			model: 'figcaption',
 			view: 'figcaption'
-		} ) );
+		} );
 	}
 }
 

+ 8 - 11
packages/ckeditor5-engine/tests/manual/selection.js

@@ -5,8 +5,6 @@
 
 /* global console */
 
-import { downcastElementToElement } from '../../src/conversion/downcast-converters';
-
 import { getData } from '../../src/dev-utils/model';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
@@ -16,7 +14,6 @@ import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Undo from '@ckeditor/ckeditor5-undo/src/undo';
-import { upcastElementToElement } from '../../src/conversion/upcast-converters';
 
 import './selection.css';
 import { toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';
@@ -44,24 +41,24 @@ class SelectionTest extends Plugin {
 			isLimit: true
 		} );
 
-		editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'table', view: 'table' } ) );
-		editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
-		editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
+		editor.conversion.for( 'upcast' ).elementToElement( { model: 'table', view: 'table' } );
+		editor.conversion.for( 'upcast' ).elementToElement( { model: 'tableRow', view: 'tr' } );
+		editor.conversion.for( 'upcast' ).elementToElement( { model: 'tableCell', view: 'td' } );
 
-		editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
+		editor.conversion.for( 'downcast' ).elementToElement( {
 			model: 'table',
 			view: ( modelItem, viewWriter ) => {
 				return toWidget( viewWriter.createContainerElement( 'table' ), viewWriter );
 			}
-		} ) );
-		editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
+		} );
+		editor.conversion.for( 'downcast' ).elementToElement( { model: 'tableRow', view: 'tr' } );
 
-		editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
+		editor.conversion.for( 'downcast' ).elementToElement( {
 			model: 'tableCell',
 			view: ( modelItem, viewWriter ) => {
 				return toWidgetEditable( viewWriter.createEditableElement( 'td' ), viewWriter );
 			}
-		} ) );
+		} );
 	}
 }
 

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

@@ -11,14 +11,6 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Range from '../../../../src/model/range';
 import LivePosition from '../../../../src/model/liveposition';
 
-import {
-	upcastElementToAttribute
-} from '../../../../src/conversion/upcast-converters';
-
-import {
-	downcastAttributeToElement,
-} from '../../../../src/conversion/downcast-converters';
-
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
@@ -31,20 +23,20 @@ class Link extends Plugin {
 		// Allow bold attribute on all inline nodes.
 		editor.model.schema.extend( '$text', { allowAttributes: 'link' } );
 
-		editor.conversion.for( 'downcast' ).add( downcastAttributeToElement( {
+		editor.conversion.for( 'downcast' ).attributeToElement( {
 			model: 'link',
 			view: ( modelAttributeValue, viewWriter ) => {
 				return viewWriter.createAttributeElement( 'a', { href: modelAttributeValue } );
 			}
-		} ) );
+		} );
 
-		editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( {
+		editor.conversion.for( 'upcast' ).elementToAttribute( {
 			view: 'a',
 			model: {
 				key: 'link',
 				value: viewElement => viewElement.getAttribute( 'href' )
 			}
-		} ) );
+		} );
 	}
 }
 

+ 4 - 5
packages/ckeditor5-engine/tests/manual/tickets/ckeditor5-721/1.js

@@ -13,7 +13,6 @@ import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 
 import ViewPosition from '../../../../src/view/position';
-import { downcastElementToElement } from '../../../../src/conversion/downcast-converters';
 import { setData } from '../../../../src/dev-utils/model';
 
 ClassicEditor
@@ -41,7 +40,7 @@ ClassicEditor
 		} );
 
 		editor.conversion.for( 'downcast' )
-			.add( downcastElementToElement( {
+			.elementToElement( {
 				model: 'widget',
 				view: ( modelItem, writer ) => {
 					const b = writer.createAttributeElement( 'b' );
@@ -51,11 +50,11 @@ ClassicEditor
 
 					return toWidget( div, writer, { label: 'element label' } );
 				}
-			} ) )
-			.add( downcastElementToElement( {
+			} )
+			.elementToElement( {
 				model: 'nested',
 				view: ( item, writer ) => writer.createEditableElement( 'figcaption', { contenteditable: true } )
-			} ) );
+			} );
 
 		setData( editor.model,
 			'<paragraph>foo[]</paragraph>' +

+ 4 - 12
packages/ckeditor5-engine/tests/tickets/699.js

@@ -8,14 +8,6 @@
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
-import {
-	upcastElementToElement
-} from '../../src/conversion/upcast-converters';
-
-import {
-	downcastElementToElement
-} from '../../src/conversion/downcast-converters';
-
 import { getData as getModelData } from '../../src/dev-utils/model';
 import { getData as getViewData } from '../../src/dev-utils/view';
 
@@ -54,13 +46,13 @@ function WidgetPlugin( editor ) {
 	} );
 	schema.extend( 'widget', { allowIn: '$root' } );
 
-	editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
+	editor.conversion.for( 'downcast' ).elementToElement( {
 		model: 'widget',
 		view: 'widget'
-	} ) );
+	} );
 
-	editor.conversion.for( 'upcast' ).add( upcastElementToElement( {
+	editor.conversion.for( 'upcast' ).elementToElement( {
 		model: 'widget',
 		view: 'widget'
-	} ) );
+	} );
 }

+ 3 - 4
packages/ckeditor5-engine/tests/utils/bindtwostepcarettoattribute.js

@@ -11,7 +11,6 @@ import DomEventData from '../../src/view/observer/domeventdata';
 import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
 import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
 import Position from '../../src/model/position';
-import { upcastElementToAttribute } from '../../src/conversion/upcast-converters';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { setData } from '../../src/dev-utils/model';
 
@@ -37,9 +36,9 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 			} );
 
 			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
-			editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'a', model: 'a' } ) );
-			editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'b', model: 'b' } ) );
-			editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'c', model: 'c' } ) );
+			editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'a', model: 'a' } );
+			editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'b', model: 'b' } );
+			editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
 			editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 
 			bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'a' );

Неке датотеке нису приказане због велике количине промена