浏览代码

Merge pull request #1274 from ckeditor/t/1236

Other: Conversion utilities refactor. Closes #1236.

---

### Additional information

* `ModelConverterBuilder` and `ViewConverterBuilder` are removed,
* `definition-based-converters` are removed,
* `conversion.Conversion` class is introduced,
* new converter functions, basing on declarative config, are introduced,
* other related changes.
Piotr Jasiun 7 年之前
父节点
当前提交
5851811113
共有 44 个文件被更改,包括 4732 次插入4358 次删除
  1. 33 48
      packages/ckeditor5-engine/src/controller/datacontroller.js
  2. 17 25
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  3. 0 420
      packages/ckeditor5-engine/src/conversion/buildmodelconverter.js
  4. 0 550
      packages/ckeditor5-engine/src/conversion/buildviewconverter.js
  5. 145 0
      packages/ckeditor5-engine/src/conversion/conversion.js
  6. 0 347
      packages/ckeditor5-engine/src/conversion/definition-based-converters.js
  7. 1074 0
      packages/ckeditor5-engine/src/conversion/downcast-converters.js
  8. 4 4
      packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js
  9. 33 33
      packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js
  10. 0 616
      packages/ckeditor5-engine/src/conversion/model-to-view-converters.js
  11. 4 4
      packages/ckeditor5-engine/src/conversion/modelconsumable.js
  12. 400 0
      packages/ckeditor5-engine/src/conversion/two-way-converters.js
  13. 567 0
      packages/ckeditor5-engine/src/conversion/upcast-converters.js
  14. 1 1
      packages/ckeditor5-engine/src/conversion/view-selection-to-model-converters.js
  15. 91 75
      packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
  16. 0 60
      packages/ckeditor5-engine/src/conversion/view-to-model-converters.js
  17. 42 29
      packages/ckeditor5-engine/src/dev-utils/model.js
  18. 14 5
      packages/ckeditor5-engine/src/model/markercollection.js
  19. 25 5
      packages/ckeditor5-engine/src/view/element.js
  20. 58 0
      packages/ckeditor5-engine/src/view/elementdefinition.jsdoc
  21. 103 46
      packages/ckeditor5-engine/src/view/matcher.js
  22. 0 44
      packages/ckeditor5-engine/src/view/viewelementdefinition.jsdoc
  23. 4 0
      packages/ckeditor5-engine/src/view/writer.js
  24. 31 24
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  25. 14 11
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  26. 0 604
      packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js
  27. 0 631
      packages/ckeditor5-engine/tests/conversion/buildviewconverter.js
  28. 73 0
      packages/ckeditor5-engine/tests/conversion/conversion.js
  29. 0 484
      packages/ckeditor5-engine/tests/conversion/definition-based-converters.js
  30. 547 21
      packages/ckeditor5-engine/tests/conversion/model-to-view-converters.js
  31. 6 6
      packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js
  32. 5 5
      packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js
  33. 537 0
      packages/ckeditor5-engine/tests/conversion/two-way-converters.js
  34. 783 0
      packages/ckeditor5-engine/tests/conversion/upcast-converters.js
  35. 1 1
      packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js
  36. 8 8
      packages/ckeditor5-engine/tests/conversion/viewconversiondispatcher.js
  37. 0 185
      packages/ckeditor5-engine/tests/conversion/view-to-model-converters.js
  38. 28 20
      packages/ckeditor5-engine/tests/manual/highlight.js
  39. 9 5
      packages/ckeditor5-engine/tests/manual/markers.js
  40. 34 20
      packages/ckeditor5-engine/tests/manual/nestededitable.js
  41. 17 12
      packages/ckeditor5-engine/tests/manual/tickets/475/1.js
  42. 15 8
      packages/ckeditor5-engine/tests/tickets/699.js
  43. 8 0
      packages/ckeditor5-engine/tests/view/element.js
  44. 1 1
      packages/ckeditor5-engine/tests/view/manual/uielement.js

+ 33 - 48
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -12,11 +12,11 @@ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 
 import Mapper from '../conversion/mapper';
 
-import ModelConversionDispatcher from '../conversion/modelconversiondispatcher';
-import { insertText } from '../conversion/model-to-view-converters';
+import DowncastDispatcher from '../conversion/downcastdispatcher';
+import { insertText } from '../conversion/downcast-converters';
 
-import ViewConversionDispatcher from '../conversion/viewconversiondispatcher';
-import { convertText, convertToModelFragment } from '../conversion/view-to-model-converters';
+import UpcastDispatcher from '../conversion/upcastdispatcher';
+import { convertText, convertToModelFragment } from '../conversion/upcast-converters';
 
 import ViewDocumentFragment from '../view/documentfragment';
 
@@ -26,11 +26,11 @@ import ModelRange from '../model/range';
  * Controller for the data pipeline. The data pipeline controls how data is retrieved from the document
  * and set inside it. Hence, the controller features two methods which allow to {@link ~DataController#get get}
  * and {@link ~DataController#set set} data of the {@link ~DataController#model model}
- * using the given:
+ * using given:
  *
  * * {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor},
- * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher model to view} and
- * * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher view to model} converters.
+ * * downcast converters,
+ * * upcast converters.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
@@ -62,44 +62,30 @@ export default class DataController {
 		/**
 		 * Mapper used for the conversion. It has no permanent bindings, because they are created when getting data and
 		 * cleared directly after the data are converted. However, the mapper is defined as a class property, because
-		 * it needs to be passed to the `ModelConversionDispatcher` as a conversion API.
+		 * it needs to be passed to the `DowncastDispatcher` as a conversion API.
 		 *
 		 * @member {module:engine/conversion/mapper~Mapper}
 		 */
 		this.mapper = new Mapper();
 
 		/**
-		 * Model-to-view conversion dispatcher used by the {@link #get get method}.
-		 * To attach the model-to-view converter to the data pipeline you need to add a listener to this property:
-		 *
-		 *		data.modelToView( 'insert:$element', customInsertConverter );
-		 *
-		 * Or use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}:
-		 *
-		 *		buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
+		 * Downcast dispatcher used by the {@link #get get method}. Downcast converters should be attached to it.
 		 *
 		 * @readonly
-		 * @member {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}
+		 * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher}
 		 */
-		this.modelToView = new ModelConversionDispatcher( this.model, {
+		this.downcastDispatcher = new DowncastDispatcher( this.model, {
 			mapper: this.mapper
 		} );
-		this.modelToView.on( 'insert:$text', insertText(), { priority: 'lowest' } );
+		this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
 
 		/**
-		 * View-to-model conversion dispatcher used by the {@link #set set method}.
-		 * To attach the view-to-model converter to the data pipeline you need to add a listener to this property:
-		 *
-		 *		data.viewToModel( 'element', customElementConverter );
-		 *
-		 * Or use {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder}:
-		 *
-		 *		buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', 'true' );
+		 * Upcast dispatcher used by the {@link #set set method}. Upcast converters should be attached to it.
 		 *
 		 * @readonly
-		 * @member {module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}
+		 * @member {module:engine/conversion/upcastdispatcher~UpcastDispatcher}
 		 */
-		this.viewToModel = new ViewConversionDispatcher( this.model, {
+		this.upcastDispatcher = new UpcastDispatcher( this.model, {
 			schema: model.schema
 		} );
 
@@ -108,13 +94,13 @@ export default class DataController {
 		// Note that if there is no default converter for the element it will be skipped, for instance `<b>foo</b>` will be
 		// converted to nothing. We add `convertToModelFragment` as a last converter so it converts children of that
 		// element to the document fragment so `<b>foo</b>` will be converted to `foo` if there is no converter for `<b>`.
-		this.viewToModel.on( 'text', convertText(), { priority: 'lowest' } );
-		this.viewToModel.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
-		this.viewToModel.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
+		this.upcastDispatcher.on( 'text', convertText(), { priority: 'lowest' } );
+		this.upcastDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+		this.upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
 	}
 
 	/**
-	 * Returns the model's data converted by the {@link #modelToView model-to-view converters} and
+	 * Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and
 	 * formatted by the {@link #processor data processor}.
 	 *
 	 * @param {String} [rootName='main'] Root name.
@@ -127,9 +113,8 @@ export default class DataController {
 
 	/**
 	 * Returns the content of the given {@link module:engine/model/element~Element model's element} or
-	 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the
-	 * {@link #modelToView model-to-view converters} and formatted by the
-	 * {@link #processor data processor}.
+	 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the downcast converters
+	 * attached to {@link #downcastDispatcher} and formatted by the {@link #processor data processor}.
 	 *
 	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
 	 * Element whose content will be stringified.
@@ -145,8 +130,8 @@ export default class DataController {
 
 	/**
 	 * Returns the content of the given {@link module:engine/model/element~Element model element} or
-	 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the
-	 * {@link #modelToView model-to-view converters} to a
+	 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the downcast
+	 * converters attached to {@link #downcastDispatcher} to a
 	 * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}.
 	 *
 	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
@@ -160,7 +145,7 @@ export default class DataController {
 		const viewDocumentFragment = new ViewDocumentFragment();
 		this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
 
-		this.modelToView.convertInsert( modelRange );
+		this.downcastDispatcher.convertInsert( modelRange );
 
 		if ( !modelElementOrFragment.is( 'documentFragment' ) ) {
 			// Then, if a document element is converted, convert markers.
@@ -168,7 +153,7 @@ export default class DataController {
 			const markers = _getMarkersRelativeToElement( modelElementOrFragment );
 
 			for ( const [ name, range ] of markers ) {
-				this.modelToView.convertMarkerAdd( name, range );
+				this.downcastDispatcher.convertMarkerAdd( name, range );
 			}
 		}
 
@@ -180,7 +165,7 @@ export default class DataController {
 
 	/**
 	 * Sets input data parsed by the {@link #processor data processor} and
-	 * converted by the {@link #viewToModel view-to-model converters}.
+	 * converted by the {@link #upcastDispatcher view-to-model converters}.
 	 *
 	 * This method also creates a batch with all the changes applied. If all you need is to parse data, use
 	 * the {@link #parse} method.
@@ -204,13 +189,13 @@ export default class DataController {
 	}
 
 	/**
-	 * Returns the data parsed by the {@link #processor data processor} and then
-	 * converted by the {@link #viewToModel view-to-model converters}.
+	 * Returns the data parsed by the {@link #processor data processor} and then converted by upcast converters
+	 * attached to the {@link #upcastDispatcher}.
 	 *
 	 * @see #set
 	 * @param {String} data Data to parse.
 	 * @param {module:engine/model/schema~SchemaContextDefinition} [context='$root'] Base context in which the view will
-	 * be converted to the model. See: {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
+	 * be converted to the model. See: {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#convert}.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Parsed data.
 	 */
 	parse( data, context = '$root' ) {
@@ -224,7 +209,7 @@ export default class DataController {
 	/**
 	 * Returns the result of the given {@link module:engine/view/element~Element view element} or
 	 * {@link module:engine/view/documentfragment~DocumentFragment view document fragment} converted by the
-	 * {@link #viewToModel view-to-model converters}, wrapped by {module:engine/model/documentfragment~DocumentFragment}.
+	 * {@link #upcastDispatcher view-to-model converters}, wrapped by {module:engine/model/documentfragment~DocumentFragment}.
 	 *
 	 * When marker elements were converted during the conversion process, it will be set as a document fragment's
 	 * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
@@ -232,11 +217,11 @@ export default class DataController {
 	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
 	 * Element or document fragment whose content will be converted.
 	 * @param {module:engine/model/schema~SchemaContextDefinition} [context='$root'] Base context in which the view will
-	 * be converted to the model. See: {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
+	 * be converted to the model. See: {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#convert}.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
 	 */
 	toModel( viewElementOrFragment, context = '$root' ) {
-		return this.viewToModel.convert( viewElementOrFragment, context );
+		return this.upcastDispatcher.convert( viewElementOrFragment, context );
 	}
 
 	/**
@@ -247,7 +232,7 @@ export default class DataController {
 
 mix( DataController, ObservableMixin );
 
-// Helper function for converting part of a model to view.
+// Helper function for downcast conversion.
 //
 // Takes a document element (element that is added to a model document) and checks which markers are inside it
 // and which markers are containing it. If the marker is intersecting with element, the intersection is returned.

+ 17 - 25
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -10,18 +10,18 @@
 import RootEditableElement from '../view/rooteditableelement';
 import ViewDocument from '../view/document';
 import Mapper from '../conversion/mapper';
-import ModelConversionDispatcher from '../conversion/modelconversiondispatcher';
+import DowncastDispatcher from '../conversion/downcastdispatcher';
 import {
 	insertText,
 	remove
-} from '../conversion/model-to-view-converters';
-import { convertSelectionChange } from '../conversion/view-selection-to-model-converters';
+} from '../conversion/downcast-converters';
+import { convertSelectionChange } from '../conversion/upcast-selection-converters';
 import {
 	convertRangeSelection,
 	convertCollapsedSelection,
 	clearAttributes,
 	clearFakeSelection
-} from '../conversion/model-selection-to-view-converters';
+} from '../conversion/downcast-selection-converters';
 
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
@@ -65,20 +65,12 @@ export default class EditingController {
 		this.mapper = new Mapper();
 
 		/**
-		 * Model-to-view conversion dispatcher that converts changes from the model to {@link #view the editing view}.
-		 *
-		 * To attach the model-to-view converter to the editing pipeline you need to add a listener to this dispatcher:
-		 *
-		 *		editing.modelToView( 'insert:$element', customInsertConverter );
-		 *
-		 * Or use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}:
-		 *
-		 *		buildModelConverter().for( editing.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
+		 * Downcast dispatcher that converts changes from the model to {@link #view the editing view}.
 		 *
 		 * @readonly
-		 * @member {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} #modelToView
+		 * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #downcastDispatcher
 		 */
-		this.modelToView = new ModelConversionDispatcher( this.model, {
+		this.downcastDispatcher = new DowncastDispatcher( this.model, {
 			mapper: this.mapper,
 			viewSelection: this.view.selection
 		} );
@@ -86,11 +78,11 @@ export default class EditingController {
 		const doc = this.model.document;
 
 		this.listenTo( doc, 'change', () => {
-			this.modelToView.convertChanges( doc.differ );
+			this.downcastDispatcher.convertChanges( doc.differ );
 		}, { priority: 'low' } );
 
 		this.listenTo( model, '_change', () => {
-			this.modelToView.convertSelection( doc.selection );
+			this.downcastDispatcher.convertSelection( doc.selection );
 			this.view.render();
 		}, { priority: 'low' } );
 
@@ -98,14 +90,14 @@ export default class EditingController {
 		this.listenTo( this.view, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );
 
 		// Attach default model converters.
-		this.modelToView.on( 'insert:$text', insertText(), { priority: 'lowest' } );
-		this.modelToView.on( 'remove', remove(), { priority: 'low' } );
+		this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
+		this.downcastDispatcher.on( 'remove', remove(), { priority: 'low' } );
 
 		// Attach default model selection converters.
-		this.modelToView.on( 'selection', clearAttributes(), { priority: 'low' } );
-		this.modelToView.on( 'selection', clearFakeSelection(), { priority: 'low' } );
-		this.modelToView.on( 'selection', convertRangeSelection(), { priority: 'low' } );
-		this.modelToView.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
+		this.downcastDispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
+		this.downcastDispatcher.on( 'selection', clearFakeSelection(), { priority: 'low' } );
+		this.downcastDispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } );
+		this.downcastDispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
 
 		// Convert markers removal.
 		//
@@ -130,7 +122,7 @@ export default class EditingController {
 				if ( _operationAffectsMarker( operation, marker ) ) {
 					// And if the operation in any way modifies the marker, remove the marker from the view.
 					removedMarkers.add( marker.name );
-					this.modelToView.convertMarkerRemove( marker.name, markerRange );
+					this.downcastDispatcher.convertMarkerRemove( marker.name, markerRange );
 
 					// TODO: This stinks but this is the safest place to have this code.
 					this.model.document.differ.bufferMarkerChange( marker.name, markerRange, markerRange );
@@ -143,7 +135,7 @@ export default class EditingController {
 		this.listenTo( model.markers, 'remove', ( evt, marker ) => {
 			if ( !removedMarkers.has( marker.name ) ) {
 				removedMarkers.add( marker.name );
-				this.modelToView.convertMarkerRemove( marker.name, marker.getRange() );
+				this.downcastDispatcher.convertMarkerRemove( marker.name, marker.getRange() );
 			}
 		} );
 

+ 0 - 420
packages/ckeditor5-engine/src/conversion/buildmodelconverter.js

@@ -1,420 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/conversion/buildmodelconverter
- */
-
-import {
-	insertElement,
-	insertUIElement,
-	removeUIElement,
-	changeAttribute,
-	wrap,
-	highlightText,
-	highlightElement,
-	removeHighlight
-} from './model-to-view-converters';
-
-import ViewAttributeElement from '../view/attributeelement';
-import ViewContainerElement from '../view/containerelement';
-import ViewUIElement from '../view/uielement';
-
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
-/**
- * Provides chainable, high-level API to easily build basic model-to-view converters that are appended to given
- * dispatchers. In many cases, this is the API that should be used to specify how abstract model elements and
- * attributes should be represented in the view (and then later in DOM). Instances of this class are created by
- * {@link module:engine/conversion/buildmodelconverter~buildModelConverter}.
- *
- * If you need more complex converters, see {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher},
- * {@link module:engine/conversion/model-to-view-converters}, {@link module:engine/conversion/modelconsumable~ModelConsumable},
- * {@link module:engine/conversion/mapper~Mapper}.
- *
- * Using this API it is possible to create five kinds of converters:
- *
- * 1. Model element to view element converter. This is a converter that takes the model element and represents it
- * in the view.
- *
- *		buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
- *		buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( 'img' );
- *
- * 2. Model attribute to view attribute converter. This is a converter that operates on model element attributes
- * and converts them to view element attributes. It is suitable for elements like `image` (`src`, `title` attributes).
- *
- *		buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( 'img' );
- *		buildModelConverter().for( dispatcher ).fromAttribute( 'src' ).toAttribute();
- *
- * 3. Model attribute to view element converter. This is a converter that takes model attributes and represents them
- * as view elements. Elements created by this kind of converter are wrapping other view elements. Wrapped view nodes
- * correspond to model nodes had converter attribute. It is suitable for attributes like `bold`, where `bold` attribute
- * set on model text nodes is converter to `strong` view element.
- *
- *		buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
- *
- * 4. Model marker to view highlight converter. This is a converter that converts model markers to view highlight
- * described by {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object passed to
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toHighlight} method.
- *
- *		buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( {
- *			class: 'search',
- *			priority: 20
- *		} );
- *
- * 5. Model marker to element converter. This is a converter that takes model marker and creates separate elements at
- * the beginning and at the end of the marker's range. For more information see
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement} method.
- *
- *		buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
- *
- * It is possible to provide various different parameters for
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement},
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toAttribute} and
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toHighlight} methods.
- * See their descriptions to learn more.
- *
- * It is also possible to {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority change default priority}
- * of created converters to decide which converter should be fired earlier and which later. This is useful if you have
- * a general converter but also want to provide different special-case converters (i.e. given model element is converted
- * always to given view element, but if it has given attribute it is converter to other view element). For this,
- * use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority withPriority} right after `from...` method.
- *
- * Note that `to...` methods are "terminators", which means that should be the last one used in building converter.
- *
- * You can use {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder}
- * to create "opposite" converters - from view to model.
- */
-class ModelConverterBuilder {
-	/**
-	 * Creates `ModelConverterBuilder` with given `dispatchers` registered to it.
-	 */
-	constructor() {
-		/**
-		 * Dispatchers to which converters will be attached.
-		 *
-		 * @type {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>}
-		 * @private
-		 */
-		this._dispatchers = [];
-
-		/**
-		 * Contains data about registered "from" query.
-		 *
-		 * @type {Object}
-		 * @private
-		 */
-		this._from = null;
-	}
-
-	/**
-	 * Set one or more dispatchers which the built converter will be attached to.
-	 *
-	 * @chainable
-	 * @param {...module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} dispatchers One or more dispatchers.
-	 * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
-	 */
-	for( ...dispatchers ) {
-		this._dispatchers = dispatchers;
-
-		return this;
-	}
-
-	/**
-	 * Registers what model element should be converted.
-	 *
-	 * @chainable
-	 * @param {String} elementName Name of element to convert.
-	 * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
-	 */
-	fromElement( elementName ) {
-		this._from = {
-			type: 'element',
-			name: elementName,
-			priority: null
-		};
-
-		return this;
-	}
-
-	/**
-	 * Registers what model attribute should be converted.
-	 *
-	 * @chainable
-	 * @param {String} key Key of attribute to convert.
-	 * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
-	 */
-	fromAttribute( key ) {
-		this._from = {
-			type: 'attribute',
-			key,
-			priority: null
-		};
-
-		return this;
-	}
-
-	/**
-	 * Registers what type of marker should be converted.
-	 *
-	 * @chainable
-	 * @param {String} markerName Name of marker to convert.
-	 * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
-	 */
-	fromMarker( markerName ) {
-		this._from = {
-			type: 'marker',
-			name: markerName,
-			priority: null
-		};
-
-		return this;
-	}
-
-	/**
-	 * Changes default priority for built converter. The lower the number, the earlier converter will be fired.
-	 * Default priority is `10`.
-	 *
-	 * **Note:** Keep in mind that event priority, that is set by this modifier, is used for attribute priority
-	 * when {@link module:engine/view/writer~writer} is used. This changes how view elements are ordered,
-	 * i.e.: `<strong><em>foo</em></strong>` vs `<em><strong>foo</strong></em>`. Using priority you can also
-	 * prevent node merging, i.e.: `<span class="bold"><span class="theme">foo</span><span>` vs `<span class="bold theme">foo</span>`.
-	 * If you want to prevent merging, just set different priority for both converters.
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).withPriority( 2 ).toElement( 'strong' );
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'italic' ).withPriority( 3 ).toElement( 'em' );
-	 *
-	 * @chainable
-	 * @param {Number} priority Converter priority.
-	 * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
-	 */
-	withPriority( priority ) {
-		this._from.priority = priority;
-
-		return this;
-	}
-
-	/**
-	 * Registers what view element will be created by converter.
-	 *
-	 * Method accepts various ways of providing how the view element will be created. You can pass view element name as
-	 * `string`, view element instance which will be cloned and used, or creator function which returns view element that
-	 * will be used. Keep in mind that when you view element instance or creator function, it has to be/return a
-	 * proper type of view element: {@link module:engine/view/containerelement~ContainerElement ViewContainerElement} if you convert
-	 * from element, {@link module:engine/view/attributeelement~AttributeElement ViewAttributeElement} if you convert
-	 * from attribute and {@link module:engine/view/uielement~UIElement ViewUIElement} if you convert from marker.
-	 *
-	 * **Note:** When converting from model's marker, separate elements will be created at the beginning and at the end of the
-	 * marker's range. If range is collapsed then only one element will be created. See how markers
-	 * {module:engine/model/buildviewconverter~ViewConverterBuilder#toMarker serialization from view to model}
-	 * works to find out what view element format is the best for you.
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( new ViewContainerElement( 'img' ) );
-	 *
-	 *		buildModelConverter().for( dispatcher )
-	 *			.fromElement( 'header' )
-	 *			.toElement( ( data ) => new ViewContainerElement( 'h' + data.item.getAttribute( 'level' ) ) );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( new ViewAttributeElement( 'strong' ) );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( new ViewUIElement( 'span' ) );
-	 *
-	 * Creator function will be passed different values depending whether conversion is from element or from attribute:
-	 *
-	 * * from element: dispatcher's
-	 * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}
-	 * parameters will be passed,
-	 * * from attribute: there is one parameter and it is attribute value,
-	 * * from marker: {@link module:engine/conversion/buildmodelconverter~MarkerViewElementCreatorData}.
-	 *
-	 * This method also registers model selection to view selection converter, if conversion is from attribute.
-	 *
-	 * This method creates the converter and adds it as a callback to a proper
-	 * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher conversion dispatcher} event.
-	 *
-	 * @param {String|module:engine/view/element~Element|Function} element Element created by converter or
-	 * a function that returns view element.
-	 */
-	toElement( element ) {
-		const priority = this._from.priority === null ? 'normal' : this._from.priority;
-
-		for ( const dispatcher of this._dispatchers ) {
-			if ( this._from.type == 'element' ) {
-				// From model element to view element -> insert element.
-				element = typeof element == 'string' ? new ViewContainerElement( element ) : element;
-
-				dispatcher.on( 'insert:' + this._from.name, insertElement( element ), { priority } );
-			} else if ( this._from.type == 'attribute' ) {
-				// From model attribute to view element -> wrap.
-				element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
-
-				dispatcher.on( 'attribute:' + this._from.key, wrap( element ), { priority } );
-			} else {
-				// From marker to element.
-				const priority = this._from.priority === null ? 'normal' : this._from.priority;
-
-				element = typeof element == 'string' ? new ViewUIElement( element ) : element;
-
-				dispatcher.on( 'addMarker:' + this._from.name, insertUIElement( element ), { priority } );
-				dispatcher.on( 'removeMarker:' + this._from.name, removeUIElement( element ), { priority } );
-			}
-		}
-	}
-
-	/**
-	 * Registers that marker should be converted to view highlight. Markers, basically,
-	 * are {@link module:engine/model/liverange~LiveRange} instances, that are named. View highlight is
-	 * a representation of the model marker in the view:
-	 * * each {@link module:engine/view/text~Text view text node} in the marker's range will be wrapped with `span`
-	 * {@link module:engine/view/attributeelement~AttributeElement},
-	 * * each {@link module:engine/view/containerelement~ContainerElement container view element} in the marker's
-	 * range can handle highlighting individually by providing `addHighlight` and `removeHighlight`
-	 * custom properties:
-	 *
-	 *		viewElement.setCustomProperty( 'addHighlight', ( element, descriptor ) => {} );
-	 *		viewElement.setCustomProperty( 'removeHighlight', ( element, descriptorId ) => {} );
-	 *
-	 * {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} will be used to create
-	 * spans over text nodes and also will be provided to `addHighlight` and `removeHighlight` methods
-	 * each time highlight should be set or removed from view elements.
-	 *
-	 * **Note:** When `addHighlight` and `removeHighlight` custom properties are present, converter assumes
-	 * that element itself is taking care of presenting highlight on its child nodes, so it won't convert them.
-	 *
-	 * Highlight descriptor can be provided as plain object:
-	 *
-	 *		buildModelConverter.for( dispatcher ).fromMarker( 'search' ).toHighlight( { class: 'search-highlight' } );
- 	 *
-	 * Also, descriptor creator function can be provided:
-	 *
-	 *		buildModelConverter.for( dispatcher ).fromMarker( 'search:blue' ).toHighlight( data => {
-	 *			const color = data.markerName.split( ':' )[ 1 ];
-	 *
-	 *			return { class: 'search-' + color };
-	 *		} );
-	 *
-	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
-	 * `build-model-converter-non-marker-to-highlight` when trying to convert not from marker.
-	 *
-	 * @param {function|module:engine/conversion/model-to-view-converters~HighlightDescriptor} highlightDescriptor
-	 */
-	toHighlight( highlightDescriptor ) {
-		const priority = this._from.priority === null ? 'normal' : this._from.priority;
-
-		if ( this._from.type != 'marker' ) {
-			/**
-			 * Conversion to a highlight is supported only from model markers.
-			 *
-			 * @error build-model-converter-non-marker-to-highlight
-			 */
-			throw new CKEditorError(
-				'build-model-converter-non-marker-to-highlight: Conversion to a highlight is supported ' +
-				'only from model markers.'
-			);
-		}
-
-		for ( const dispatcher of this._dispatchers ) {
-			dispatcher.on( 'addMarker:' + this._from.name, highlightText( highlightDescriptor ), { priority } );
-			dispatcher.on( 'addMarker:' + this._from.name, highlightElement( highlightDescriptor ), { priority } );
-
-			dispatcher.on( 'removeMarker:' + this._from.name, removeHighlight( highlightDescriptor ), { priority } );
-		}
-	}
-
-	/**
-	 * Registers what view attribute will be created by converter. Keep in mind, that only model attribute to
-	 * view attribute conversion is supported.
-	 *
-	 * Method accepts various ways of providing how the view attribute will be created:
-	 *
-	 * * for no passed parameter, attribute key and value will be converted 1-to-1 to view attribute,
-	 * * if you pass one `string`, it will be used as new attribute key while attribute value will be copied,
-	 * * if you pass two `string`s, first one will be used as new attribute key and second one as new attribute value,
-	 * * if you pass a function, it is expected to return an object with `key` and `value` properties representing attribute key and value.
-	 * This function will be passed model attribute value and model attribute key as first two parameters and then
-	 * all dispatcher's
-	 * {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute changeAttribute event}
-	 * parameters.
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'class' ).toAttribute( '' );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'linkTitle' ).toAttribute( 'title' );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'highlighted' ).toAttribute( 'style', 'background:yellow' );
-	 *
-	 *		buildModelConverter().for( dispatcher )
-	 *			.fromAttribute( 'theme' )
-	 *			.toAttribute( ( value ) => ( { key: 'class', value: value + '-theme' } ) );
-	 *
-	 * This method creates the converter and adds it as a callback to a proper
-	 * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher conversion dispatcher} event.
-	 *
-	 * @param {String|Function} [keyOrCreator] Attribute key or a creator function.
-	 * @param {*} [value] Attribute value.
-	 */
-	toAttribute( keyOrCreator, value ) {
-		if ( this._from.type != 'attribute' ) {
-			/**
-			 * To-attribute conversion is supported only for model attributes.
-			 *
-			 * @error build-model-converter-element-to-attribute
-			 */
-			throw new CKEditorError( 'build-model-converter-non-attribute-to-attribute: ' +
-				'To-attribute conversion is supported only from model attributes.' );
-		}
-
-		let attributeCreator;
-
-		if ( !keyOrCreator ) {
-			// If `keyOrCreator` is not set, we assume default behavior which is 1:1 attribute re-write.
-			// This is also a default behavior for `changeAttribute` converter when no attribute creator is passed.
-			attributeCreator = undefined;
-		} else if ( typeof keyOrCreator == 'string' ) {
-			// `keyOrCreator` is an attribute key.
-
-			if ( value ) {
-				// If value is set, create "dumb" creator that always returns the same object.
-				attributeCreator = function() {
-					return { key: keyOrCreator, value };
-				};
-			} else {
-				// If value is not set, take it from the passed parameter.
-				attributeCreator = function( value ) {
-					return { key: keyOrCreator, value };
-				};
-			}
-		} else {
-			// `keyOrCreator` is an attribute creator function.
-			attributeCreator = keyOrCreator;
-		}
-
-		for ( const dispatcher of this._dispatchers ) {
-			const options = { priority: this._from.priority || 'normal' };
-
-			dispatcher.on( 'attribute:' + this._from.key, changeAttribute( attributeCreator ), options );
-		}
-	}
-}
-
-/**
- * Entry point for model-to-view converters builder. This chainable API makes it easy to create basic, most common
- * model-to-view converters and attach them to provided dispatchers. The method returns an instance of
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}.
- */
-export default function buildModelConverter() {
-	return new ModelConverterBuilder();
-}
-
-/**
- * @typedef {Object} module:engine/conversion/buildmodelconverter~MarkerViewElementCreatorData
- *
- * @param {String} markerName Marker name.
- * @param {module:engine/model/range~Range} markerRange Marker range.
- * @param {Boolean} isOpening Defines if currently converted element is a beginning or end of the marker range.
- */

+ 0 - 550
packages/ckeditor5-engine/src/conversion/buildviewconverter.js

@@ -1,550 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/conversion/buildviewconverter
- */
-
-import Matcher from '../view/matcher';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import Position from '../model/position';
-import Range from '../model/range';
-
-/**
- * Provides chainable, high-level API to easily build basic view-to-model converters that are appended to given
- * dispatchers. View-to-model converters are used when external data is added to the editor, i.e. when a user pastes
- * HTML content to the editor. Then, converters are used to translate this structure, possibly removing unknown/incorrect
- * nodes, and add it to the model. Also multiple, different elements might be translated into the same thing in the
- * model, i.e. `<b>` and `<strong>` elements might be converted to `bold` attribute (even though `bold` attribute will
- * be then converted only to `<strong>` tag). Instances of this class are created by
- * {@link module:engine/conversion/buildviewconverter~buildViewConverter}.
- *
- * If you need more complex converters, see {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher},
- * {@link module:engine/conversion/view-to-model-converters}, {@link module:engine/conversion/viewconsumable~ViewConsumable}.
- *
- * Using this API it is possible to create various kind of converters:
- *
- * 1. View element to model element:
- *
- *		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
- *
- * 2. View element to model attribute:
- *
- *		buildViewConverter().for( dispatcher ).fromElement( 'b' ).fromElement( 'strong' ).toAttribute( 'bold', 'true' );
- *
- * 3. View attribute to model attribute:
- *
- *		buildViewConverter().for( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttribute( 'bold', 'true' );
- *		buildViewConverter().for( dispatcher )
- *			.fromAttribute( 'class' )
- *			.toAttribute( ( viewElement ) => ( { class: viewElement.getAttribute( 'class' ) } ) );
- *
- * 4. View elements and attributes to model attribute:
- *
- *		buildViewConverter().for( dispatcher )
- *			.fromElement( 'b' ).fromElement( 'strong' ).fromAttribute( 'style', { 'font-weight': 'bold' } )
- *			.toAttribute( 'bold', 'true' );
- *
- * 5. View {@link module:engine/view/matcher~Matcher view element matcher instance} or
- * {@link module:engine/view/matcher~Matcher#add matcher pattern}
- * to model element or attribute:
- *
- *		const matcher = new ViewMatcher();
- *		matcher.add( 'div', { class: 'quote' } );
- *		buildViewConverter().for( dispatcher ).from( matcher ).toElement( 'quote' );
- *
- *		buildViewConverter().for( dispatcher ).from( { name: 'span', class: 'bold' } ).toAttribute( 'bold', 'true' );
- *
- * Note, that converters built using `ViewConverterBuilder` automatically check {@link module:engine/model/schema~Schema schema}
- * if created model structure is valid. If given conversion would be invalid according to schema, it is ignored.
- *
- * It is possible to provide creator functions as parameters for {@link ~ViewConverterBuilder#toElement}
- * and {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder#toAttribute} methods. See their descriptions to learn more.
- *
- * By default, converter will {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume} every value specified in
- * given `from...` query, i.e. `.from( { name: 'span', class: 'bold' } )` will make converter consume both `span` name
- * and `bold` class. It is possible to change this behavior using {@link ~ViewConverterBuilder#consuming consuming}
- * modifier. The modifier alters the last `fromXXX` query used before it. To learn more about consuming values,
- * see {@link module:engine/conversion/viewconsumable~ViewConsumable}.
- *
- * It is also possible to {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder#withPriority change default priority}
- * of created converters to decide which converter should be fired earlier and which later. This is useful if you provide
- * a general converter but want to provide different converter for a specific-case (i.e. given view element is converted
- * always to given model element, but if it has given class it is converter to other model element). For this,
- * use {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder#withPriority withPriority} modifier. The modifier alters
- * the last `from...` query used before it.
- *
- * Note that `to...` methods are "terminators", which means that should be the last one used in building converter.
- *
- * You can use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
- * to create "opposite" converters - from model to view.
- */
-class ViewConverterBuilder {
-	/**
-	 * Creates `ViewConverterBuilder` with given `dispatchers` registered to it.
-	 */
-	constructor() {
-		/**
-		 * Dispatchers to which converters will be attached.
-		 *
-		 * @type {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>}
-		 * @private
-		 */
-		this._dispatchers = [];
-
-		/**
-		 * Stores "from" queries.
-		 *
-		 * @type {Array}
-		 * @private
-		 */
-		this._from = [];
-	}
-
-	/**
-	 * Set one or more dispatchers which the built converter will be attached to.
-	 *
-	 * @chainable
-	 * @param {...module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher} dispatchers One or more dispatchers.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	for( ...dispatchers ) {
-		this._dispatchers = dispatchers;
-
-		return this;
-	}
-
-	/**
-	 * Registers what view element should be converted.
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-	 *
-	 * @chainable
-	 * @param {String} elementName View element name.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	fromElement( elementName ) {
-		return this.from( { name: elementName } );
-	}
-
-	/**
-	 * Registers what view attribute should be converted.
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttribute( 'bold', 'true' );
-	 *
-	 * @chainable
-	 * @param {String|RegExp} key View attribute key.
-	 * @param {String|RegExp} [value] View attribute value.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	fromAttribute( key, value = /.*/ ) {
-		const pattern = {};
-
-		if ( key === 'style' || key === 'class' ) {
-			pattern[ key ] = value;
-		} else {
-			pattern.attribute = {};
-			pattern.attribute[ key ] = value;
-		}
-
-		const matcher = new Matcher( pattern );
-
-		this._from.push( {
-			matcher,
-			consume: false,
-			priority: null,
-			attributeKey: key
-		} );
-
-		return this;
-	}
-
-	/**
-	 * Registers what view pattern should be converted. The method accepts either {@link module:engine/view/matcher~Matcher view matcher}
-	 * or view matcher pattern.
-	 *
-	 *		const matcher = new ViewMatcher();
-	 *		matcher.add( 'div', { class: 'quote' } );
-	 *		buildViewConverter().for( dispatcher ).from( matcher ).toElement( 'quote' );
-	 *
-	 *		buildViewConverter().for( dispatcher ).from( { name: 'span', class: 'bold' } ).toAttribute( 'bold', 'true' );
-	 *
-	 * @chainable
-	 * @param {Object|module:engine/view/matcher~Matcher} matcher View matcher or view matcher pattern.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	from( matcher ) {
-		if ( !( matcher instanceof Matcher ) ) {
-			matcher = new Matcher( matcher );
-		}
-
-		this._from.push( {
-			matcher,
-			consume: false,
-			priority: null
-		} );
-
-		return this;
-	}
-
-	/**
-	 * Modifies which consumable values will be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}
-	 * by built converter.
-	 * It modifies the last `from...` query. Can be used after each `from...` query in given chain. Useful for providing
-	 * more specific matches.
-	 *
-	 *		// This converter will only handle class bold conversion (to proper attribute) but span element
-	 *		// conversion will have to be done in separate converter.
-	 *		// Without consuming modifier, the converter would consume both class and name, so a converter for
-	 *		// span element would not be fired.
-	 *		buildViewConverter().for( dispatcher )
-	 *			.from( { name: 'span', class: 'bold' } ).consuming( { class: 'bold' } )
-	 *			.toAttribute( 'bold', 'true' } );
-	 *
-	 *		buildViewConverter().for( dispatcher )
-	 *			.fromElement( 'img' ).consuming( { name: true, attribute: [ 'src', 'title' ] } )
-	 *			.toElement( ( viewElement ) => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ),
-	 *			                                                            title: viewElement.getAttribute( 'title' ) } );
-	 *
-	 * **Note:** All and only values from passed object has to be consumable on converted view element. This means that
-	 * using `consuming` method, you can either make looser conversion conditions (like in first example) or tighter
-	 * conversion conditions (like in second example). So, the view element, to be converter, has to match query of
-	 * `from...` method and then have to have enough consumable values to consume.
-	 *
-	 * @see module:engine/conversion/viewconsumable~ViewConsumable
-	 * @chainable
-	 * @param {Object} consume Values to consume.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	consuming( consume ) {
-		const lastFrom = this._from[ this._from.length - 1 ];
-		lastFrom.consume = consume;
-
-		return this;
-	}
-
-	/**
-	 * Changes default priority for built converter. It modifies the last `from...` query. Can be used after each
-	 * `from...` query in given chain. Useful for overwriting converters. The lower the number, the earlier converter will be fired.
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-	 *		// Register converter with proper priority, otherwise "p" element would get consumed by first
-	 *		// converter and the second converter would not be fired.
-	 *		buildViewConverter().for( dispatcher )
-	 *			.from( { name: 'p', class: 'custom' } ).withPriority( 9 )
-	 *			.toElement( 'customParagraph' );
-	 *
-	 * **Note:** `ViewConverterBuilder` takes care of applying all `toElement()` conversions before all `toAttribute()`
-	 * conversions. This is done by setting default `toElement()` priority to `normal` and `toAttribute()` priority to `low`.
-	 * It is recommended to set converter priority for `toElement()` around `0` (the value of `normal` priority)
-	 * and `toAttribute()` priority around `-1000` (the value of `low` priority).
-	 * It is important that model elements are created before attributes, otherwise attributes would
-	 * not be applied or other errors may occur.
-	 *
-	 * @chainable
-	 * @param {Number} priority Converter priority.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	withPriority( priority ) {
-		const lastFrom = this._from[ this._from.length - 1 ];
-		lastFrom.priority = priority;
-
-		return this;
-	}
-
-	/**
-	 * Registers what model element will be created by converter.
-	 *
-	 * Method accepts two ways of providing what kind of model element will be created. You can pass model element
-	 * name as a `string` or a function that will return model element instance. If you provide creator function,
-	 * it will be passed converted view element as first and only parameter.
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-	 *		buildViewConverter().for( dispatcher )
-	 *			.fromElement( 'img' )
-	 *			.toElement( ( viewElement, writer ) => writer.createElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) );
-	 *
-	 * @param {String|Function} element Model element name or model element creator function.
-	 */
-	toElement( element ) {
-		function eventCallbackGen( from ) {
-			return ( evt, data, conversionApi ) => {
-				const writer = conversionApi.writer;
-
-				// There is one callback for all patterns in the matcher.
-				// This will be usually just one pattern but we support matchers with many patterns too.
-				const matchAll = from.matcher.matchAll( data.viewItem );
-
-				// If there is no match, this callback should not do anything.
-				if ( !matchAll ) {
-					return;
-				}
-
-				// Now, for every match between matcher and actual element, we will try to consume the match.
-				for ( const match of matchAll ) {
-					// Create model element basing on creator function or element name.
-					const modelElement = element instanceof Function ? element( data.viewItem, writer ) : writer.createElement( element );
-
-					// Do not convert if element building function returned falsy value.
-					if ( !modelElement ) {
-						continue;
-					}
-
-					// When element was already consumed then skip it.
-					if ( !conversionApi.consumable.test( data.viewItem, from.consume || match.match ) ) {
-						continue;
-					}
-
-					// 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 ) {
-						continue;
-					}
-
-					// Insert element on allowed position.
-					conversionApi.writer.insert( modelElement, splitResult.position );
-
-					// Convert children and insert to element.
-					const childrenResult = conversionApi.convertChildren( data.viewItem, Position.createAt( modelElement ) );
-
-					// Consume appropriate value from consumable values list.
-					conversionApi.consumable.consume( data.viewItem, from.consume || match.match );
-
-					// Set conversion result range.
-					data.modelRange = new Range(
-						// Range should start before inserted element
-						Position.createBefore( modelElement ),
-						// Should end after but we need to take into consideration that children could split our
-						// element, so we need to move range after parent of the last converted child.
-						// before: <allowed>[]</allowed>
-						// after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
-						Position.createAfter( 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 = Position.createAt( splitResult.cursorParent );
-
-					// Otherwise just continue after inserted element.
-					} else {
-						data.modelCursor = data.modelRange.end;
-					}
-
-					// Prevent multiple conversion if there are other correct matches.
-					break;
-				}
-			};
-		}
-
-		this._setCallback( eventCallbackGen, 'normal' );
-	}
-
-	/**
-	 * Registers what model attribute will be created by converter.
-	 *
-	 * Method accepts two ways of providing what kind of model attribute will be created. You can either pass two strings
-	 * representing attribute key and attribute value or a function that returns an object with `key` and `value` properties.
-	 * If you provide creator function, it will be passed converted view element as first and only parameter.
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromAttribute( 'alt' ).toAttribute( 'alt' );
-	 *		buildViewConverter().for( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttribute( 'bold', true );
-	 *		buildViewConverter().for( dispatcher )
-	 *			.fromAttribute( 'class' )
-	 *			.toAttribute( ( viewElement ) => ( { key: 'class', value: 'class-' + viewElement.getAttribute( 'class' ) } ) );
-	 *
-	 * @param {String|Function} keyOrCreator Attribute key or a creator function.
-	 * @param {String} [value] Attribute value. Ignored if `keyOrCreator` is not a `string`. If `keyOrCreator` is `string`,
-	 * if `value` is not set, attribute value from converted element will be used.
-	 */
-	toAttribute( keyOrCreator, value ) {
-		function eventCallbackGen( from ) {
-			return ( evt, data, conversionApi ) => {
-				// There is one callback for all patterns in the matcher.
-				// This will be usually just one pattern but we support matchers with many patterns too.
-				const matchAll = from.matcher.matchAll( data.viewItem );
-
-				// If there is no match, this callback should not do anything.
-				if ( !matchAll ) {
-					return;
-				}
-
-				// Now, for every match between matcher and actual element, we will try to consume the match.
-				for ( const match of matchAll ) {
-					// Try to consume appropriate values from consumable values list.
-					if ( !conversionApi.consumable.consume( data.viewItem, from.consume || match.match ) ) {
-						continue;
-					}
-
-					// 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 ) );
-					}
-
-					// Use attribute creator function, if provided.
-					let attribute;
-
-					if ( keyOrCreator instanceof Function ) {
-						attribute = keyOrCreator( data.viewItem );
-
-						if ( !attribute ) {
-							return;
-						}
-					} else {
-						attribute = {
-							key: keyOrCreator,
-							value: value ? value : data.viewItem.getAttribute( from.attributeKey )
-						};
-					}
-
-					// Set attribute on each item in range according to Schema.
-					for ( const node of Array.from( data.modelRange.getItems() ) ) {
-						if ( conversionApi.schema.checkAttribute( node, attribute.key ) ) {
-							conversionApi.writer.setAttribute( attribute.key, attribute.value, node );
-						}
-					}
-
-					// Prevent multiple conversion if there are other correct matches.
-					break;
-				}
-			};
-		}
-
-		this._setCallback( eventCallbackGen, 'low' );
-	}
-
-	/**
-	 * Registers how model element marking marker range will be created by converter.
-	 *
-	 * Created element has to match the following pattern:
-	 *
-	 * 		{ name: '$marker', attribute: { data-name: /^\w/ } }
-	 *
-	 * There are two ways of creating this element:
-	 *
-	 * 1. Makes sure that converted view element will have property `data-name` then converter will
-	 * automatically take this property value. In this case there is no need to provide creator function.
-	 * For the following view:
-	 *
-	 *		<marker data-name="search"></marker>foo<marker data-name="search"></marker>
-	 *
-	 * converter should look like this:
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromElement( 'marker' ).toMarker();
-	 *
-	 * 2. Creates element by creator:
-	 *
-	 * For the following view:
-	 *
-	 * 		<span foo="search"></span>foo<span foo="search"></span>
-	 *
-	 * converter should look like this:
-	 *
-	 * 		buildViewConverter().for( dispatcher ).from( { name: 'span', { attribute: foo: /^\w/ } } ).toMarker( ( data ) => {
-	 * 			return new Element( '$marker', { 'data-name': data.getAttribute( 'foo' ) } );
-	 * 		} );
-	 *
-	 * @param {Function} [creator] Creator function.
-	 */
-	toMarker( creator ) {
-		function eventCallbackGen( from ) {
-			return ( evt, data, conversionApi ) => {
-				const writer = conversionApi.writer;
-
-				// There is one callback for all patterns in the matcher.
-				// This will be usually just one pattern but we support matchers with many patterns too.
-				const matchAll = from.matcher.matchAll( data.viewItem );
-
-				// If there is no match, this callback should not do anything.
-				if ( !matchAll ) {
-					return;
-				}
-
-				let modelElement;
-
-				// When creator is provided then create model element basing on creator function.
-				if ( creator instanceof Function ) {
-					modelElement = creator( data.viewItem );
-				// When there is no creator then create model element basing on data from view element.
-				} else {
-					modelElement = writer.createElement( '$marker', { 'data-name': data.viewItem.getAttribute( 'data-name' ) } );
-				}
-
-				// Check if model element is correct (has proper name and property).
-				if ( modelElement.name != '$marker' || typeof modelElement.getAttribute( 'data-name' ) != 'string' ) {
-					throw new CKEditorError(
-						'build-view-converter-invalid-marker: Invalid model element to mark marker range.'
-					);
-				}
-
-				// Now, for every match between matcher and actual element, we will try to consume the match.
-				for ( const match of matchAll ) {
-					// Try to consume appropriate values from consumable values list.
-					if ( !conversionApi.consumable.consume( data.viewItem, from.consume || match.match ) ) {
-						continue;
-					}
-
-					// Tmp fix because multiple matchers are not properly matched and consumed.
-					// See https://github.com/ckeditor/ckeditor5-engine/issues/1257.
-					if ( data.modelRange ) {
-						continue;
-					}
-
-					writer.insert( modelElement, data.modelCursor );
-					data.modelRange = Range.createOn( modelElement );
-					data.modelCursor = data.modelRange.end;
-
-					// Prevent multiple conversion if there are other correct matches.
-					break;
-				}
-			};
-		}
-
-		this._setCallback( eventCallbackGen, 'normal' );
-	}
-
-	/**
-	 * Helper function that uses given callback generator to created callback function and sets it on registered dispatchers.
-	 *
-	 * @param eventCallbackGen
-	 * @param defaultPriority
-	 * @private
-	 */
-	_setCallback( eventCallbackGen, defaultPriority ) {
-		// We will add separate event callback for each registered `from` entry.
-		for ( const from of this._from ) {
-			// We have to figure out event name basing on matcher's patterns.
-			// If there is exactly one pattern and it has `name` property we will used that name.
-			const matcherElementName = from.matcher.getElementName();
-			const eventName = matcherElementName ? 'element:' + matcherElementName : 'element';
-			const eventCallback = eventCallbackGen( from );
-
-			const priority = from.priority === null ? defaultPriority : from.priority;
-
-			// Add event to each registered dispatcher.
-			for ( const dispatcher of this._dispatchers ) {
-				dispatcher.on( eventName, eventCallback, { priority } );
-			}
-		}
-	}
-}
-
-/**
- * Entry point for view-to-model converters builder. This chainable API makes it easy to create basic, most common
- * view-to-model converters and attach them to provided dispatchers. The method returns an instance of
- * {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder}.
- */
-export default function buildViewConverter() {
-	return new ViewConverterBuilder();
-}

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

@@ -0,0 +1,145 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/conversion/conversion
+ */
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+/**
+ * An utility class that helps organizing dispatchers and adding converters to them.
+ */
+export default class Conversion {
+	/**
+	 * Creates new Conversion instance.
+	 */
+	constructor() {
+		this._dispatchersGroups = new Map();
+	}
+
+	/**
+	 * Registers one or more converters under given group name. Then, group name can be used to assign a converter
+	 * to multiple dispatchers at once.
+	 *
+	 * If given group name is used for a second time,
+	 * {@link module:utils/ckeditorerror~CKEditorError conversion-register-group-exists} error is thrown.
+	 *
+	 * @param {String} groupName A name for dispatchers group.
+	 * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers Dispatchers to register
+	 * under given name.
+	 */
+	register( groupName, dispatchers ) {
+		if ( this._dispatchersGroups.has( groupName ) ) {
+			/**
+			 * Trying to register a group name that was already registered.
+			 *
+			 * @error conversion-register-group-exists
+			 */
+			throw new CKEditorError( 'conversion-register-group-exists: Trying to register a group name that was already registered.' );
+		}
+
+		this._dispatchersGroups.set( groupName, dispatchers );
+	}
+
+	/**
+	 * Provides chainable API to assign converters to dispatchers registered under given group name. Converters are added
+	 * by calling `.add()` method of an object returned by this function.
+	 *
+	 *		conversion.for( 'downcast' )
+	 *			.add( conversionHelperA )
+	 *			.add( conversionHelperB );
+	 *
+	 * In above example, `conversionHelperA` and `conversionHelperB` will be called for all dispatchers from `'model'` group.
+	 *
+	 * `.add()` takes exactly one parameter, which is a function. That function should accept one parameter, which
+	 * is a dispatcher instance. The function should add an actual converter to passed dispatcher instance.
+	 *
+	 * Conversion helpers for most common cases are already provided. They are flexible enough to cover most use cases.
+	 * See documentation to learn how they can be configured.
+	 *
+	 * 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}.
+	 *
+	 * 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}.
+	 *
+	 * An example of using conversion helpers to convert `paragraph` model element to `p` view element (and back):
+	 *
+	 *		// Define conversion configuration - model element 'paragraph' should be converted to view element 'p'.
+	 *		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 custom conversion helper that uses custom converter function:
+	 *
+	 *		// Adding custom `myConverter` converter for 'paragraph' element insertion, with default priority ('normal').
+	 *		conversion.for( 'downcast' ).add( conversion.customConverter( 'insert:paragraph', myConverter ) );
+	 *
+	 * @param {String} groupName Name of dispatchers group to add converters to.
+	 * @returns {Object} Object with `.add()` method, providing a way to add converters.
+	 */
+	for( groupName ) {
+		const dispatchers = this._getDispatchers( groupName );
+
+		return {
+			add( conversionHelper ) {
+				_addToDispatchers( dispatchers, conversionHelper );
+
+				return this;
+			}
+		};
+	}
+
+	/**
+	 * Returns dispatchers registered under given group name.
+	 *
+	 * If given group name has not been registered,
+	 * {@link module:utils/ckeditorerror~CKEditorError conversion-for-unknown-group} error is thrown.
+	 *
+	 * @private
+	 * @param {String} groupName
+	 * @returns {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>}
+	 */
+	_getDispatchers( groupName ) {
+		const dispatchers = this._dispatchersGroups.get( groupName );
+
+		if ( !dispatchers ) {
+			/**
+			 * Trying to add a converter to an unknown dispatchers group.
+			 *
+			 * @error conversion-for-unknown-group
+			 */
+			throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' );
+		}
+
+		return dispatchers;
+	}
+}
+
+// Helper function for `Conversion` `.add()` method.
+//
+// Calls `conversionHelper` on each dispatcher from the group specified earlier in `.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 );
+	}
+}

+ 0 - 347
packages/ckeditor5-engine/src/conversion/definition-based-converters.js

@@ -1,347 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/conversion/definition-based-converters
- */
-
-import AttributeElement from '../view/attributeelement';
-import ViewContainerElement from '../view/containerelement';
-
-import buildModelConverter from './buildmodelconverter';
-import buildViewConverter from './buildviewconverter';
-
-/**
- * Helper for creating a model element to {@link module:engine/view/containerelement~ContainerElement view container element}
- * converters.
- *
- * You can create a converter by using a simplified converter definition:
- *
- *		modelElementToViewContainerElement( {
- *			model: 'heading1',
- *			view: 'h1',
- *		}, [ editor.editing.modelToView, editor.data.modelToView ] );
- *
- * Or by using a full-flavored view object:
- *
- *		modelElementToViewContainerElement( {
- *			model: 'heading1',
- *			view: {
- *				name: 'h1',
- *				class: [ 'header', 'article-header' ],
- *				attribute: {
- *					data-header: 'level-1',
- *				}
- *			},
- *		}, [ editor.editing.modelToView, editor.data.modelToView ] );
- *
- * The above will generate the following view element:
- *
- *		<h1 class="header article-header" data-header="level-1">...</h1>
- *
- * @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition The converter configuration.
- * @param {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>} dispatchers
- */
-export function modelElementToViewContainerElement( definition, dispatchers ) {
-	const { model: modelElement, targetView } = normalizeConverterDefinition( definition );
-
-	buildModelConverter()
-		.for( ...dispatchers )
-		.fromElement( modelElement )
-		.toElement( () => createViewElementFromDefinition( targetView, ViewContainerElement ) );
-}
-
-/**
- * Helper for creating a view element to model element converters.
- *
- * Besides converting the view element specified in the `view` property it will also convert all view elements
- * which match the patterns defined in the `acceptAlso` property. Such a "wide" converters are often needed so the editor
- * is able to correctly handle a content that was pasted into the editor. Such pasted content may use various
- * "flavors" of the same editing features (e.g. you may want to handle `<h1>` and `<p class="heading1">` as headings).
- *
- * The `model` property defines the model element name to be used by the converter.
- *
- * A converter can be defined using a simplified converter definition:
- *
- *		viewToModelElement( { model: 'heading1', view: 'h1' }, [ dispatcher ] );
- *
- * Or by using a full-flavored definition:
- *
- *		viewToModelElement( {
- *			model: 'heading1',
- *			view: {
- *				name: 'p',
- *				attribute: {
- *					'data-heading': 'true'
- *				},
- *				// You may need to use a high-priority listener to catch elements
- *				// which are handled by other (usually – more generic) converters too.
- *				priority: 'high'
- *			}
- *		}, [ editor.data.viewToModel ] );
- *
- * Or with the `acceptAlso` property to match more patterns:
- *
- *		viewToModelElement( {
- *			model: 'heading1',
- *			view: 'h1',
- *			acceptAlso: [
- *				{ name: 'p', attribute: { 'data-heading': 'level1' }, priority: 'high' },
- *				{ name: 'h2', class: 'heading-main' },
- *				{ name: 'div', style: { 'font-weight': 'bold', font-size: '24px' } }
- *			]
- *		}, [ editor.data.viewToModel ] );
- *
- * The above example will convert an existing view elements:
- *
- *		<h1>A heading</h1>
- *		<h2 class="heading-main">Another heading</h2>
- *		<p data-heading="level1">Paragraph-like heading</p>
- *		<div style="font-size:24px; font-weigh:bold;">Another non-semantic header</div>
- *
- * into `heading1` model elements so in model it will be represented as:
- *
- *		<heading1>A heading</heading1>
- *		<heading1>Another heading</heading1>
- *		<heading1>Paragraph-like heading</heading1>
- *		<heading1>Another non-semantic header</heading1>
- *
- * @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition A conversion configuration.
- * @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
- */
-export function viewToModelElement( definition, dispatchers ) {
-	const { model: modelElement, sourceViews } = normalizeConverterDefinition( definition );
-
-	const converter = prepareViewConverter( dispatchers, sourceViews );
-
-	converter.toElement( modelElement );
-}
-
-/**
- * Helper for creating a model attribute to {@link module:engine/view/attributeelement~AttributeElement view attribute element}
- * converters.
- *
- * A converter can be defined by using a simplified converter definition:
- *
- *		modelAttributeToViewAttributeElement( 'bold', [
- *			{
- *				model: 'true',
- *				view: 'strong'
- *			}
- *		], [ editor.editing.modelToView, editor.data.modelToView ] );
- *
- * Or defining full-flavored view objects:
- *
- *		modelAttributeToViewAttributeElement( 'fontSize', [
- *			{
- *				model: 'big',
- *				view: {
- *					name: 'span',
- *					style: { 'font-size': '1.2em' }
- *				},
- *			},
- *			{
- *				model: 'small',
- *				view: {
- *					name: 'span',
- *					style: { 'font-size': '0.8em' }
- *				},
- *			}
- *		], [ editor.editing.modelToView, editor.data.modelToView ] );
- *
- * The above will generate the following view element from a `fontSize="big"` model attribute:
- *
- *		<span style="font-size: 1.2em">...</span>
- *
- * @param {String} attributeName The name of the model attribute which should be converted.
- * @param {Array.<module:engine/conversion/definition-based-converters~ConverterDefinition>} definitions A conversion configuration objects
- * for each possible attribute value.
- * @param {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>} dispatchers
- */
-export function modelAttributeToViewAttributeElement( attributeName, definitions, dispatchers ) {
-	// Create a map of attributeValue - to - ViewElementDefinition.
-	const valueToTargetViewMap = definitions
-		.map( normalizeConverterDefinition )
-		.reduce( ( mapObject, normalizedDefinition ) => {
-			mapObject[ normalizedDefinition.model ] = normalizedDefinition.targetView;
-
-			return mapObject;
-		}, {} );
-
-	buildModelConverter()
-		.for( ...dispatchers )
-		.fromAttribute( attributeName )
-		.toElement( value => {
-			const targetView = valueToTargetViewMap[ value ];
-
-			if ( !targetView ) {
-				return;
-			}
-
-			return createViewElementFromDefinition( targetView, AttributeElement );
-		} );
-}
-
-/**
- * Helper for creating view element to model attribute converters.
- *
- * Besides converting the view element specified in the `view` property it will also convert all view elements
- * which match the patterns defined in the `acceptAlso` property. Such "wide" converters are often needed so the editor
- * is able to correctly handle a content that was pasted into the editor. Such pasted content may use various
- * "flavors" of the same editing features (e.g. bold might be represented as `<b>`, `<strong>` or
- * `<span style="font-weight:bold">`).
- *
- * The `model` property defines the value of the model attribute.
- *
- * A converter can be defined using a simplified converter definition:
- *
- *		viewToModelAttribute( 'bold', { model: true, view: 'strong' }, [ dispatcher ] );
- *
- * Or by using a full-flavored definition:
- *
- *		viewToModelAttribute( 'fontSize', {
- *			model: 'big',
- *			view: {
- *				name: 'span',
- *				style: {
- *					'font-size': '1.2em'
- *				}
- *			}
- *		}, [ editor.data.viewToModel ] );
- *
- * Or with the `acceptAlso` property to match more patterns:
- *
- *		viewToModelAttribute( 'fontSize', {
- *			model: 'big',
- *			view: {
- *				name: 'span',
- *				class: 'text-big'
- *			},
- *			acceptAlso: [
- *				{ name: 'span', attribute: { 'data-size': 'big' } },
- *				{ name: 'span', class: [ 'font', 'font-huge' ] },
- *				{ name: 'span', style: { font-size: '18px' } }
- *			]
- *		}, [ editor.data.viewToModel ] );
- *
- * The above example will convert the following view elements:
- *
- *		<p>
- *			An example text with some big elements:
- *			<span class="text-big">one</span>,
- *			<span data-size="big">two</span>,
- *			<span class="font font-huge">three</span>,
- *			<span style="font-size: 18px">four</span>
- *		</p>
- *
- * to a `fontSize="big"` model attribute:
- *
- *		<paragraph>
- *			An example text with some big elements:
- *			<$text fontSize="big">one</$text>,
- *			<$text fontSize="big">two</$text>,
- *			<$text fontSize="big">three</$text>,
- *			<$text fontSize="big">four</$text>
- *		</paragraph>
- *
- * @param {String} attributeName Attribute name to which convert.
- * @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition A conversion configuration.
- * @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
- */
-export function viewToModelAttribute( attributeName, definition, dispatchers ) {
-	const { model: attributeValue, sourceViews } = normalizeConverterDefinition( definition );
-
-	const converter = prepareViewConverter( dispatchers, sourceViews );
-
-	converter.toAttribute( () => ( {
-		key: attributeName,
-		value: attributeValue
-	} ) );
-}
-
-// Normalize a {@link module:engine/conversion/definition-based-converters~ConverterDefinition}
-// into internal object used when building converters.
-//
-// @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition An object that defines view to model
-// and model to view conversion.
-// @returns {Object}
-function normalizeConverterDefinition( definition ) {
-	const model = definition.model;
-	const view = definition.view;
-
-	// View definition might be defined as a name of an element.
-	const targetView = typeof view == 'string' ? { name: view } : view;
-
-	const sourceViews = Array.from( definition.acceptsAlso ? definition.acceptsAlso : [] );
-
-	// Source views also accepts default view definition used in model-to-view conversion.
-	sourceViews.push( targetView );
-
-	return { model, targetView, sourceViews };
-}
-
-// Helper method for preparing a view converter from passed view definitions.
-//
-// @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
-// @param {Array.<module:engine/view/viewelementdefinition~ViewElementDefinition>} viewDefinitions
-// @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-function prepareViewConverter( dispatchers, viewDefinitions ) {
-	const converter = buildViewConverter().for( ...dispatchers );
-
-	for ( const viewDefinition of viewDefinitions ) {
-		converter.from( Object.assign( {}, viewDefinition ) );
-
-		if ( viewDefinition.priority ) {
-			converter.withPriority( viewDefinition.priority );
-		}
-	}
-
-	return converter;
-}
-
-// Creates view element instance from provided viewElementDefinition and class.
-//
-// @param {module:engine/view/viewelementdefinition~ViewElementDefinition} viewElementDefinition
-// @param {Function} ViewElementClass
-// @returns {module:engine/view/element~Element}
-function createViewElementFromDefinition( viewElementDefinition, ViewElementClass ) {
-	const element = new ViewElementClass( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
-
-	if ( viewElementDefinition.style ) {
-		element.setStyle( viewElementDefinition.style );
-	}
-
-	const classes = viewElementDefinition.class;
-
-	if ( classes ) {
-		element.addClass( ... typeof classes === 'string' ? [ classes ] : classes );
-	}
-
-	return element;
-}
-
-/**
- * Defines conversion details. It is used in configuration-based converters:
- *
- * * {@link module:engine/conversion/definition-based-converters~modelAttributeToViewAttributeElement}
- * * {@link module:engine/conversion/definition-based-converters~modelElementToViewContainerElement}
- * * {@link module:engine/conversion/definition-based-converters~viewToModelAttribute}
- * * {@link module:engine/conversion/definition-based-converters~viewToModelElement}
- *
- * See the above converters for examples how to use the converter definition.
- *
- * @typedef {Object} ConverterDefinition
- * @property {String} model Defines to model conversion. When using to element conversion
- * ({@link module:engine/conversion/definition-based-converters~viewToModelElement}
- * and {@link module:engine/conversion/definition-based-converters~modelElementToViewContainerElement})
- * it defines element name. When using to attribute conversion
- * ({@link module:engine/conversion/definition-based-converters~viewToModelAttribute}
- * and {@link module:engine/conversion/definition-based-converters~modelAttributeToViewAttributeElement})
- * it defines attribute value to which it is converted.
- * @property {module:engine/view/viewelementdefinition~ViewElementDefinition} view Defines model to view conversion and is also used
- * in view to model conversion pipeline.
- * @property {Array.<module:engine/view/viewelementdefinition~ViewElementDefinition>} acceptAlso An array with all matched elements that
- * view to model conversion should also accepts.
- */

文件差异内容过多而无法显示
+ 1074 - 0
packages/ckeditor5-engine/src/conversion/downcast-converters.js


+ 4 - 4
packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js

@@ -8,9 +8,9 @@ import viewWriter from '../view/writer';
 /**
  * Contains {@link module:engine/model/selection~Selection model selection} to
  * {@link module:engine/view/selection~Selection view selection} converters for
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}.
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
  *
- * @module engine/conversion/model-selection-to-view-converters
+ * @module engine/conversion/downcast-selection-converters
  */
 
 /**
@@ -62,7 +62,7 @@ export function convertRangeSelection() {
  * converted, the broken attributes might be merged again, or the position where the selection is may be wrapped
  * in different, appropriate attribute elements.
  *
- * See also {@link module:engine/conversion/model-selection-to-view-converters~clearAttributes} which does a clean-up
+ * See also {@link module:engine/conversion/downcast-selection-converters~clearAttributes} which does a clean-up
  * by merging attributes.
  *
  * @returns {Function} Selection converter.
@@ -106,7 +106,7 @@ export function convertCollapsedSelection() {
  *
  *		modelDispatcher.on( 'selection', clearAttributes() );
  *
- * See {@link module:engine/conversion/model-selection-to-view-converters~convertCollapsedSelection}
+ * See {@link module:engine/conversion/downcast-selection-converters~convertCollapsedSelection}
  * which do the opposite by breaking attributes in the selection position.
  *
  * @returns {Function} Selection converter.

+ 33 - 33
packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module engine/conversion/modelconversiondispatcher
+ * @module engine/conversion/downcastdispatcher
  */
 
 import Consumable from './modelconsumable';
@@ -14,69 +14,69 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
 
 /**
- * `ModelConversionDispatcher` is a central point of model conversion, which is a process of reacting to changes
+ * `DowncastDispatcher` is a central point of downcasting (conversion from model to view), which is a process of reacting to changes
  * in the model and firing a set of events. Callbacks listening to those events are called converters. Those
  * converters role is to convert the model changes to changes in view (for example, adding view nodes or
  * changing attributes on view elements).
  *
- * During conversion process, `ModelConversionDispatcher` fires events, basing on state of the model and prepares
+ * During conversion process, `DowncastDispatcher` fires events, basing on state of the model and prepares
  * data for those events. It is important to understand that those events are connected with changes done on model,
- * for example: "node has been inserted" or "attribute has changed". This is in contrary to view to model conversion,
+ * for example: "node has been inserted" or "attribute has changed". This is in a contrary to upcasting (view to model conversion),
  * where we convert view state (view nodes) to a model tree.
  *
  * The events are prepared basing on a diff created by {@link module:engine/model/differ~Differ Differ}, which buffers them
- * and then passes to `ModelConversionDispatcher` as a diff between old model state and new model state.
+ * and then passes to `DowncastDispatcher` as a diff between old model state and new model state.
  *
  * Note, that because changes are converted there is a need to have a mapping between model structure and view structure.
- * To map positions and elements during model to view conversion use {@link module:engine/conversion/mapper~Mapper}.
+ * To map positions and elements during downcast (model to view conversion) use {@link module:engine/conversion/mapper~Mapper}.
  *
- * `ModelConversionDispatcher` fires following events for model tree changes:
+ * `DowncastDispatcher` fires following events for model tree changes:
  *
- * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert}
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert}
  * if a range of nodes has been inserted to the model tree,
- * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:remove remove}
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove remove}
  * if a range of nodes has been removed from the model tree,
- * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute attribute}
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute}
  * if attribute has been added, changed or removed from a model node.
  *
- * For {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert}
- * and {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute attribute},
- * `ModelConversionDispatcher` generates {@link module:engine/conversion/modelconsumable~ModelConsumable consumables}.
+ * For {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert}
+ * and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute},
+ * `DowncastDispatcher` generates {@link module:engine/conversion/modelconsumable~ModelConsumable consumables}.
  * These are used to have a control over which changes has been already consumed. It is useful when some converters
  * overwrite other or converts multiple changes (for example converts insertion of an element and also converts that
  * element's attributes during insertion).
  *
- * Additionally, `ModelConversionDispatcher` fires events for {@link module:engine/model/markercollection~Marker marker} changes:
+ * Additionally, `DowncastDispatcher` fires events for {@link module:engine/model/markercollection~Marker marker} changes:
  *
- * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addMarker} if a marker has been added,
- * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:removeMarker} if a marker has been removed.
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} if a marker has been added,
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} if a marker has been removed.
  *
  * Note, that changing a marker is done through removing the marker from the old range, and adding on the new range,
  * so both those events are fired.
  *
- * Finally, `ModelConversionDispatcher` also handles firing events for {@link module:engine/model/selection model selection}
+ * Finally, `DowncastDispatcher` also handles firing events for {@link module:engine/model/selection model selection}
  * conversion:
  *
- * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:selection}
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:selection}
  * which converts selection from model to view,
- * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute}
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute}
  * which is fired for every selection attribute,
- * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addMarker}
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}
  * which is fired for every marker which contains selection.
  *
  * Unlike model tree and markers, events for selection are not fired for changes but for selection state.
  *
- * When providing custom listeners for `ModelConversionDispatcher` remember to check whether given change has not been
+ * When providing custom listeners for `DowncastDispatcher` remember to check whether given change has not been
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} yet.
  *
- * When providing custom listeners for `ModelConversionDispatcher` keep in mind that any callback that had
+ * When providing custom listeners for `DowncastDispatcher` keep in mind that any callback that had
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from a consumable and
  * converted the change should also stop the event (for efficiency purposes).
  *
- * Example of a custom converter for `ModelConversionDispatcher`:
+ * Example of a custom converter for `DowncastDispatcher`:
  *
  *		// We will convert inserting "paragraph" model element into the model.
- *		modelDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
+ *		downcastDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
  *			// Remember to check whether the change has not been consumed yet and consume it.
  *			if ( consumable.consume( data.item, 'insert' ) ) {
  *				return;
@@ -98,9 +98,9 @@ import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
  *			evt.stop();
  *		} );
  */
-export default class ModelConversionDispatcher {
+export default class DowncastDispatcher {
 	/**
-	 * Creates a `ModelConversionDispatcher` instance.
+	 * Creates a `DowncastDispatcher` instance.
 	 *
 	 * @param {module:engine/model/model~Model} model Data model.
 	 * @param {Object} [conversionApi] Interface passed by dispatcher to the events calls.
@@ -443,7 +443,7 @@ export default class ModelConversionDispatcher {
 	 * @param {module:engine/model/item~Item} data.item Inserted item.
 	 * @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
 	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 
 	/**
@@ -460,7 +460,7 @@ export default class ModelConversionDispatcher {
 	 * @param {module:engine/model/position~Position} data.sourcePosition Position from where the range has been removed.
 	 * @param {module:engine/model/range~Range} data.range Removed range (in {@link module:engine/model/document~Document#graveyard
 	 * graveyard root}).
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 
 	/**
@@ -482,7 +482,7 @@ export default class ModelConversionDispatcher {
 	 * @param {*} data.attributeOldValue Attribute value before the change. This is `null` when selection attribute is converted.
 	 * @param {*} data.attributeNewValue New attribute value.
 	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 
 	/**
@@ -491,7 +491,7 @@ export default class ModelConversionDispatcher {
 	 * @event selection
 	 * @param {module:engine/model/selection~Selection} selection Selection that is converted.
 	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 
 	/**
@@ -526,7 +526,7 @@ export default class ModelConversionDispatcher {
 	 * @param {module:engine/model/range~Range} data.markerRange Marker range.
 	 * @param {String} data.markerName Marker name.
 	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 
 	/**
@@ -541,11 +541,11 @@ export default class ModelConversionDispatcher {
 	 * @param {Object} data Additional information about the change.
 	 * @param {module:engine/model/range~Range} data.markerRange Marker range.
 	 * @param {String} data.markerName Marker name.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 }
 
-mix( ModelConversionDispatcher, EmitterMixin );
+mix( DowncastDispatcher, EmitterMixin );
 
 // Helper function, checks whether change of `marker` at `modelPosition` should be converted. Marker changes are not
 // converted if they happen inside an element with custom conversion method.

+ 0 - 616
packages/ckeditor5-engine/src/conversion/model-to-view-converters.js

@@ -1,616 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import ModelRange from '../model/range';
-import ModelSelection from '../model/selection';
-import ModelElement from '../model/element';
-
-import ViewElement from '../view/element';
-import ViewAttributeElement from '../view/attributeelement';
-import ViewText from '../view/text';
-import ViewRange from '../view/range';
-import viewWriter from '../view/writer';
-import DocumentSelection from '../model/documentselection';
-
-/**
- * Contains model to view converters for
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}.
- *
- * @module engine/conversion/model-to-view-converters
- */
-
-/**
- * Function factory, creates a converter that converts node insertion changes from the model to the view.
- * The view element that will be added to the view depends on passed parameter. If {@link module:engine/view/element~Element} was passed,
- * it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the
- * dispatcher's {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}.
- * It's expected that the function returns a {@link module:engine/view/element~Element}.
- * The result of the function will be inserted to the view.
- *
- * The converter automatically consumes corresponding value from consumables list, stops the event (see
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
- *
- *		modelDispatcher.on( 'insert:paragraph', insertElement( new ViewElement( 'p' ) ) );
- *
- *		modelDispatcher.on(
- *			'insert:myElem',
- *			insertElement( ( data, consumable, conversionApi ) => {
- *				let myElem = new ViewElement( 'myElem', { myAttr: true }, new ViewText( 'myText' ) );
- *
- *				// Do something fancy with myElem using data/consumable/conversionApi ...
- *
- *				return myElem;
- *			}
- *		) );
- *
- * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which
- * will be inserted.
- * @returns {Function} Insert element event converter.
- */
-export function insertElement( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const viewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data, consumable, conversionApi );
-
-		if ( !viewElement ) {
-			return;
-		}
-
-		if ( !consumable.consume( data.item, 'insert' ) ) {
-			return;
-		}
-
-		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
-
-		conversionApi.mapper.bindElements( data.item, viewElement );
-		viewWriter.insert( viewPosition, viewElement );
-	};
-}
-
-/**
- * Function factory, creates a default model-to-view converter for text insertion changes.
- *
- * The converter automatically consumes corresponding value from consumables list and stops the event (see
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
- *
- *		modelDispatcher.on( 'insert:$text', insertText() );
- *
- * @returns {Function} Insert text event converter.
- */
-export function insertText() {
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( !consumable.consume( data.item, 'insert' ) ) {
-			return;
-		}
-
-		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
-		const viewText = new ViewText( data.item.data );
-
-		viewWriter.insert( viewPosition, viewText );
-	};
-}
-
-/**
- * Function factory, creates a default model-to-view converter for node remove changes.
- *
- *		modelDispatcher.on( 'remove', remove() );
- *
- * @returns {Function} Remove event converter.
- */
-export function remove() {
-	return ( evt, data, conversionApi ) => {
-		// Find view range start position by mapping model position at which the remove happened.
-		const viewStart = conversionApi.mapper.toViewPosition( data.position );
-
-		const modelEnd = data.position.getShiftedBy( data.length );
-		const viewEnd = conversionApi.mapper.toViewPosition( modelEnd, { isPhantom: true } );
-
-		const viewRange = new ViewRange( viewStart, viewEnd );
-
-		// Trim the range to remove in case some UI elements are on the view range boundaries.
-		const removed = viewWriter.remove( viewRange.getTrimmed() );
-
-		// After the range is removed, unbind all view elements from the model.
-		// Range inside view document fragment is used to unbind deeply.
-		for ( const child of ViewRange.createIn( removed ).getItems() ) {
-			conversionApi.mapper.unbindViewElement( child );
-		}
-	};
-}
-
-/**
- * Function factory, creates a converter that converts marker adding change to the view ui element.
- * The view ui element that will be added to the view depends on passed parameter. See {@link ~insertElement}.
- * In a case of collapsed range element will not wrap range but separate elements will be placed at the beginning
- * and at the end of the range.
- *
- * **Note:** unlike {@link ~insertElement}, the converter does not bind view element to model, because this converter
- * uses marker as "model source of data". This means that view ui element does not have corresponding model element.
- *
- * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning a view element, which
- * will be inserted.
- * @returns {Function} Insert element event converter.
- */
-export function insertUIElement( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		let viewStartElement, viewEndElement;
-
-		// Create two view elements. One will be inserted at the beginning of marker, one at the end.
-		// If marker is collapsed, only "opening" element will be inserted.
-		if ( elementCreator instanceof ViewElement ) {
-			viewStartElement = elementCreator.clone( true );
-			viewEndElement = elementCreator.clone( true );
-		} else {
-			data.isOpening = true;
-			viewStartElement = elementCreator( data, conversionApi );
-
-			data.isOpening = false;
-			viewEndElement = elementCreator( data, conversionApi );
-		}
-
-		if ( !viewStartElement || !viewEndElement ) {
-			return;
-		}
-
-		const markerRange = data.markerRange;
-
-		// Marker that is collapsed has consumable build differently that non-collapsed one.
-		// For more information see `addMarker` event description.
-		// If marker's range is collapsed - check if it can be consumed.
-		if ( markerRange.isCollapsed && !consumable.consume( markerRange, evt.name ) ) {
-			return;
-		}
-
-		// If marker's range is not collapsed - consume all items inside.
-		for ( const value of markerRange ) {
-			if ( !consumable.consume( value.item, evt.name ) ) {
-				return;
-			}
-		}
-
-		const mapper = conversionApi.mapper;
-
-		// Add "opening" element.
-		viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
-
-		// Add "closing" element only if range is not collapsed.
-		if ( !markerRange.isCollapsed ) {
-			viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
-		}
-
-		evt.stop();
-	};
-}
-
-/**
- * Function factory, creates a default model-to-view converter for removing {@link module:engine/view/uielement~UIElement ui element}
- * basing on marker remove change.
- *
- * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning
- * a view ui element, which will be used as a pattern when look for element to remove at the marker start position.
- * @returns {Function} Remove ui element converter.
- */
-export function removeUIElement( elementCreator ) {
-	return ( evt, data, conversionApi ) => {
-		let viewStartElement, viewEndElement;
-
-		// Create two view elements. One will be used to remove "opening element", the other for "closing element".
-		// If marker was collapsed, only "opening" element will be removed.
-		if ( elementCreator instanceof ViewElement ) {
-			viewStartElement = elementCreator.clone( true );
-			viewEndElement = elementCreator.clone( true );
-		} else {
-			data.isOpening = true;
-			viewStartElement = elementCreator( data, conversionApi );
-
-			data.isOpening = false;
-			viewEndElement = elementCreator( data, conversionApi );
-		}
-
-		if ( !viewStartElement || !viewEndElement ) {
-			return;
-		}
-
-		const markerRange = data.markerRange;
-
-		// When removing the ui elements, we map the model range to view twice, because that view range
-		// may change after the first clearing.
-		if ( !markerRange.isCollapsed ) {
-			viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewEndElement );
-		}
-
-		// Remove "opening" element.
-		viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewStartElement );
-
-		evt.stop();
-	};
-}
-
-/**
- * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view.
- *
- * Attributes from model are converted to the view element attributes in the view. You may provide a custom function to generate
- * a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view elements
- * attributes on 1-to-1 basis.
- *
- * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
- *
- * The converter automatically consumes corresponding value from consumables list and stops the event (see
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
- *
- *		modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( data ) => {
- *			// Change attribute key from `customAttr` to `class` in view.
- *			const key = 'class';
- *			let value = data.attributeNewValue;
- *
- *			// Force attribute value to 'empty' if the model element is empty.
- *			if ( data.item.childCount === 0 ) {
- *				value = 'empty';
- *			}
- *
- *			// Return key-value pair.
- *			return { key, value };
- *		} ) );
- *
- * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
- * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
- * The function is passed all the parameters of the
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute} event.
- * @returns {Function} Set/change attribute converter.
- */
-export function changeAttribute( attributeCreator ) {
-	attributeCreator = attributeCreator || ( ( value, key ) => ( { value, key } ) );
-
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( !consumable.consume( data.item, evt.name ) ) {
-			return;
-		}
-
-		const { key, value } = attributeCreator( data.attributeNewValue, data.attributeKey, data, consumable, conversionApi );
-
-		if ( data.attributeNewValue !== null ) {
-			conversionApi.mapper.toViewElement( data.item ).setAttribute( key, value );
-		} else {
-			conversionApi.mapper.toViewElement( data.item ).removeAttribute( key );
-		}
-	};
-}
-
-/**
- * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view.
- * Also can be used to convert selection attributes. In that case, an empty attribute element will be created and the
- * selection will be put inside it.
- *
- * Attributes from model are converted to a view element that will be wrapping those view nodes that are bound to
- * model elements having given attribute. This is useful for attributes like `bold`, which may be set on text nodes in model
- * but are represented as an element in the view:
- *
- *		[paragraph]              MODEL ====> VIEW        <p>
- *			|- a {bold: true}                             |- <b>
- *			|- b {bold: true}                             |   |- ab
- *			|- c                                          |- c
- *
- * The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and
- * the copy will become the wrapping element. If `Function` is provided, it is passed attribute value and then all the parameters of the
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute attribute event}.
- * It's expected that the function returns a {@link module:engine/view/element~Element}.
- * The result of the function will be the wrapping element.
- * When provided `Function` does not return element, then will be no conversion.
- *
- * The converter automatically consumes corresponding value from consumables list, stops the event (see
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
- *
- *		modelDispatcher.on( 'attribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
- *
- * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
- * be used for wrapping.
- * @returns {Function} Set/change attribute converter.
- */
-export function wrap( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		// Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
-		// or the attribute was removed.
-		const oldViewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.attributeOldValue, data, consumable, conversionApi );
-
-		// Create node to wrap with.
-		const newViewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.attributeNewValue, data, consumable, conversionApi );
-
-		if ( !oldViewElement && !newViewElement ) {
-			return;
-		}
-
-		if ( !consumable.consume( data.item, evt.name ) ) {
-			return;
-		}
-
-		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
-			// Selection attribute conversion.
-			viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), newViewElement, conversionApi.viewSelection );
-		} else {
-			// Node attribute conversion.
-			let viewRange = conversionApi.mapper.toViewRange( data.range );
-
-			// First, unwrap the range from current wrapper.
-			if ( data.attributeOldValue !== null ) {
-				viewRange = viewWriter.unwrap( viewRange, oldViewElement );
-			}
-
-			if ( data.attributeNewValue !== null ) {
-				viewWriter.wrap( viewRange, newViewElement );
-			}
-		}
-	};
-}
-
-/**
- * Function factory, creates converter that converts text inside marker's range. Converter wraps the text with
- * {@link module:engine/view/attributeelement~AttributeElement} created from provided descriptor.
- * See {link module:engine/conversion/model-to-view-converters~createViewElementFromHighlightDescriptor}.
- *
- * Also can be used to convert selection that is inside a marker. In that case, an empty attribute element will be
- * created and the selection will be put inside it.
- *
- * If the highlight descriptor will not provide `priority` property, `10` will be used.
- *
- * If the highlight descriptor will not provide `id` property, name of the marker will be used.
- *
- * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
- * @return {Function}
- */
-export function highlightText( highlightDescriptor ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( data.markerRange.isCollapsed ) {
-			return;
-		}
-
-		if ( !( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) && !data.item.is( 'textProxy' ) ) {
-			return;
-		}
-
-		const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
-
-		if ( !descriptor ) {
-			return;
-		}
-
-		if ( !consumable.consume( data.item, evt.name ) ) {
-			return;
-		}
-
-		const viewElement = createViewElementFromHighlightDescriptor( descriptor );
-
-		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
-			viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), viewElement, conversionApi.viewSelection );
-		} else {
-			const viewRange = conversionApi.mapper.toViewRange( data.range );
-			viewWriter.wrap( viewRange, viewElement );
-		}
-	};
-}
-
-/**
- * Converter function factory. Creates a function which applies the marker's highlight to an element inside the marker's range.
- *
- * The converter checks if an element has `addHighlight` function stored as
- * {@link module:engine/view/element~Element#setCustomProperty custom property} and, if so, uses it to apply the highlight.
- * In such case converter will consume all element's children, assuming that they were handled by element itself.
- *
- * When `addHighlight` custom property is not present, element is not converted in any special way.
- * This means that converters will proceed to convert element's child nodes.
- *
- * If the highlight descriptor will not provide `priority` property, `10` will be used.
- *
- * If the highlight descriptor will not provide `id` property, name of the marker will be used.
- *
- * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
- * @return {Function}
- */
-export function highlightElement( highlightDescriptor ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( data.markerRange.isCollapsed ) {
-			return;
-		}
-
-		if ( !( data.item instanceof ModelElement ) ) {
-			return;
-		}
-
-		const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
-
-		if ( !descriptor ) {
-			return;
-		}
-
-		if ( !consumable.test( data.item, evt.name ) ) {
-			return;
-		}
-
-		const viewElement = conversionApi.mapper.toViewElement( data.item );
-
-		if ( viewElement && viewElement.getCustomProperty( 'addHighlight' ) ) {
-			// Consume element itself.
-			consumable.consume( data.item, evt.name );
-
-			// Consume all children nodes.
-			for ( const value of ModelRange.createIn( data.item ) ) {
-				consumable.consume( value.item, evt.name );
-			}
-
-			viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor );
-		}
-	};
-}
-
-/**
- * Function factory, creates a converter that converts model marker remove to the view.
- *
- * Both text nodes and elements are handled by this converter by they are handled a bit differently.
- *
- * Text nodes are unwrapped using {@link module:engine/view/attributeelement~AttributeElement} created from provided
- * highlight descriptor. See {link module:engine/conversion/model-to-view-converters~highlightDescriptorToAttributeElement}.
- *
- * For elements, the converter checks if an element has `removeHighlight` function stored as
- * {@link module:engine/view/element~Element#setCustomProperty custom property}. If so, it uses it to remove the highlight.
- * In such case, children of that element will not be converted.
- *
- * When `removeHighlight` is not present, element is not converted in any special way.
- * Instead converter will proceed to convert element's child nodes.
- *
- * If the highlight descriptor will not provide `priority` property, `10` will be used.
- *
- * If the highlight descriptor will not provide `id` property, name of the marker will be used.
- *
- * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
- * @return {Function}
- */
-export function removeHighlight( highlightDescriptor ) {
-	return ( evt, data, conversionApi ) => {
-		// This conversion makes sense only for non-collapsed range.
-		if ( data.markerRange.isCollapsed ) {
-			return;
-		}
-
-		const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
-
-		if ( !descriptor ) {
-			return;
-		}
-
-		const viewRange = conversionApi.mapper.toViewRange( data.markerRange );
-
-		// Retrieve all items in the affected range. We will process them and remove highlight from them appropriately.
-		const items = new Set( viewRange.getItems() );
-
-		// First, iterate through all items and remove highlight from those container elements that have custom highlight handling.
-		for ( const item of items ) {
-			if ( item.is( 'containerElement' ) && item.getCustomProperty( 'removeHighlight' ) ) {
-				item.getCustomProperty( 'removeHighlight' )( item, descriptor.id );
-
-				// If container element had custom handling, remove all it's children from further processing.
-				for ( const descendant of ViewRange.createIn( item ) ) {
-					items.delete( descendant );
-				}
-			}
-		}
-
-		// Then, iterate through all other items. Look for text nodes and unwrap them. Start from the end
-		// to prevent errors when view structure changes when unwrapping (and, for example, some attributes are merged).
-		const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor );
-
-		for ( const item of Array.from( items ).reverse() ) {
-			if ( item.is( 'textProxy' ) ) {
-				viewWriter.unwrap( ViewRange.createOn( item ), viewHighlightElement );
-			}
-		}
-	};
-}
-
-// Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter.
-function _prepareDescriptor( highlightDescriptor, data, conversionApi ) {
-	// If passed descriptor is a creator function, call it. If not, just use passed value.
-	const descriptor = typeof highlightDescriptor == 'function' ?
-		highlightDescriptor( data, conversionApi ) :
-		highlightDescriptor;
-
-	if ( !descriptor ) {
-		return null;
-	}
-
-	// Apply default descriptor priority.
-	if ( !descriptor.priority ) {
-		descriptor.priority = 10;
-	}
-
-	// Default descriptor id is marker name.
-	if ( !descriptor.id ) {
-		descriptor.id = data.markerName;
-	}
-
-	return descriptor;
-}
-
-/**
- * Creates `span` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from information
- * provided by {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object. If priority
- * is not provided in descriptor - default priority will be used.
- *
- * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor
- * @return {module:engine/conversion/model-to-view-converters~HighlightAttributeElement}
- */
-export function createViewElementFromHighlightDescriptor( descriptor ) {
-	const viewElement = new HighlightAttributeElement( 'span', descriptor.attributes );
-
-	if ( descriptor.class ) {
-		const cssClasses = Array.isArray( descriptor.class ) ? descriptor.class : [ descriptor.class ];
-		viewElement.addClass( ...cssClasses );
-	}
-
-	if ( descriptor.priority ) {
-		viewElement.priority = descriptor.priority;
-	}
-
-	viewElement.setCustomProperty( 'highlightDescriptorId', descriptor.id );
-
-	return viewElement;
-}
-
-/**
- * Special kind of {@link module:engine/view/attributeelement~AttributeElement} that is created and used in
- * marker-to-highlight conversion.
- *
- * The difference between `HighlightAttributeElement` and {@link module:engine/view/attributeelement~AttributeElement}
- * is {@link module:engine/view/attributeelement~AttributeElement#isSimilar} method.
- *
- * For `HighlightAttributeElement` it checks just `highlightDescriptorId` custom property, that is set during marker-to-highlight
- * conversion basing on {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object.
- * `HighlightAttributeElement`s with same `highlightDescriptorId` property are considered similar.
- */
-class HighlightAttributeElement extends ViewAttributeElement {
-	isSimilar( otherElement ) {
-		if ( otherElement.is( 'attributeElement' ) ) {
-			return this.getCustomProperty( 'highlightDescriptorId' ) === otherElement.getCustomProperty( 'highlightDescriptorId' );
-		}
-
-		return false;
-	}
-}
-
-/**
- * Object describing how the content highlight should be created in the view.
- *
- * Each text node contained in the highlight will be wrapped with `span` element with CSS class(es), attributes and priority
- * described by this object.
- *
- * Each element can handle displaying the highlight separately by providing `addHighlight` and `removeHighlight` custom
- * properties:
- *  * `HighlightDescriptor` is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to
- *  the element,
- *  * descriptor id is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight of given
- *  id from the element.
- *
- * @typedef {Object} module:engine/conversion/model-to-view-converters~HighlightDescriptor
- *
- * @property {String|Array.<String>} class CSS class or array of classes to set. If descriptor is used to
- * create {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those classes will be set
- * on that {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element,
- * usually those class will be set on that element, however this depends on how the element converts the descriptor.
- *
- * @property {String} [id] Descriptor identifier. If not provided, defaults to converted marker's name.
- *
- * @property {Number} [priority] Descriptor priority. If not provided, defaults to `10`. If descriptor is used to create
- * {@link module:engine/view/attributeelement~AttributeElement}, it will be that element's
- * {@link module:engine/view/attributeelement~AttributeElement#priority}. If descriptor is applied to an element,
- * the priority will be used to determine which descriptor is more important.
- *
- * @property {Object} [attributes] Attributes to set. If descriptor is used to create
- * {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those attributes will be set on that
- * {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element, usually those
- * attributes will be set on that element, however this depends on how the element converts the descriptor.
- */

+ 4 - 4
packages/ckeditor5-engine/src/conversion/modelconsumable.js

@@ -15,7 +15,7 @@ import TextProxy from '../model/textproxy';
  * Consumables are various aspects of the model. A model item can be broken down into singular properties that might be
  * taken into consideration when converting that item.
  *
- * `ModelConsumable` is used by {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} while analyzing changed
+ * `ModelConsumable` is used by {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} while analyzing changed
  * parts of {@link module:engine/model/document~Document the document}. The added / changed / removed model items are broken down
  * into singular properties (the item itself and it's attributes). All those parts are saved in `ModelConsumable`. Then,
  * during conversion, when given part of model item is converted (i.e. the view element has been inserted into the view,
@@ -24,12 +24,12 @@ import TextProxy from '../model/textproxy';
  * For model items, `ModelConsumable` stores consumable values of one of following types: `insert`, `addAttribute:<attributeKey>`,
  * `changeAttribute:<attributeKey>`, `removeAttribute:<attributeKey>`.
  *
- * In most cases, it is enough to let {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}
+ * In most cases, it is enough to let {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
  * gather consumable values, so there is no need to use
- * @link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly.
+ * {@link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly.
  * However, it is important to understand how consumable values can be
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
- * See {@link module:engine/conversion/model-selection-to-view-converters default model to view converters} for more information.
+ * See {@link module:engine/conversion/downcast-selection-converters default downcast converters} for more information.
  *
  * Keep in mind, that one conversion event may have multiple callbacks (converters) attached to it. Each of those is
  * able to convert one or more parts of the model. However, when one of those callbacks actually converts

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

@@ -0,0 +1,400 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/conversion/two-way-converters
+ */
+
+import {
+	downcastElementToElement,
+	downcastAttributeToElement,
+	downcastAttributeToAttribute
+} from './downcast-converters';
+
+import {
+	upcastElementToElement,
+	upcastElementToAttribute,
+	upcastAttributeToAttribute
+} from './upcast-converters';
+
+/**
+ * Defines a conversion between the model and the view where a model element is represented as a view element (and vice versa).
+ * For example, model `<paragraph>Foo</paragraph>` is `<p>Foo</p>` in the view.
+ *
+ *		elementToElement( conversion, { model: 'paragraph', view: 'p' } );
+ *
+ *		elementToElement( conversion, {
+ *			model: 'fancyParagraph',
+ *			view: {
+ *				name: 'p',
+ *				class: 'fancy'
+ *			}
+ *		} );
+ *
+ *		elementToElement( conversion, {
+ *			model: 'paragraph',
+ *			view: 'p',
+ *			upcastAlso: [
+ *				'div',
+ *				{
+ *					// Match any name.
+ *					name: /./,
+ *					style: {
+ *						display: 'block'
+ *					}
+ *				}
+ *			]
+ *		} );
+ *
+ *		elementToElement( conversion, {
+ *			model: 'heading',
+ *			view: 'h2',
+ *			// Convert "headling-like" paragraphs to headings.
+ *			upcastAlso: viewElement => {
+ *				const fontSize = viewElement.getStyle( 'font-size' );
+ *
+ *				if ( !fontSize ) {
+ *					return null;
+ *				}
+ *
+ *				const match = fontSize.match( /(\d+)\s*px/ );
+ *
+ *				if ( !match ) {
+ *					return null;
+ *				}
+ *
+ *				const size = Number( match[ 1 ] );
+ *
+ *				if ( size > 26 ) {
+ *					return { name: true, style: [ 'font-size' ] };
+ *				}
+ *
+ *				return null;
+ *			}
+ *		} );
+ *
+ * @param {module:engine/conversion/conversion~Conversion} conversion Conversion class instance with registered conversion dispatchers.
+ * @param {Object} definition Conversion definition.
+ * @param {String} definition.model Name of the model element to convert.
+ * @param {module:engine/view/elementdefinition~ElementDefinition} definition.view Definition of a view element to convert from/to.
+ * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
+ * Any view element matching `upcastAlso` will also be converted to the given model element.
+ */
+export function elementToElement( conversion, definition ) {
+	// Set up downcast converter.
+	conversion.for( 'downcast' ).add( downcastElementToElement( definition ) );
+
+	// Set up upcast converter.
+	for ( const view of _getAllViews( definition ) ) {
+		const priority = view == definition.view ? 'normal' : 'high';
+
+		conversion.for( 'upcast' ).add( upcastElementToElement( {
+			model: definition.model,
+			view
+		}, priority ) );
+	}
+}
+
+/**
+ * Defines a conversion between the model and the view where a model attribute is represented as a view element (and vice versa).
+ * For example, model text node with data `"Foo"` and `bold` attribute is `<strong>Foo</strong>` in the view.
+ *
+ *		attributeToElement( conversion, 'bold', { view: 'strong' } );
+ *
+ *		attributeToElement( conversion, 'bold', {
+ *			view: {
+ *				name: 'span',
+ *				class: 'bold'
+ *			}
+ *		} );
+ *
+ *		attributeToElement( conversion, 'bold', {
+ *			view: 'strong',
+ *			upcastAlso: [
+ *				'b',
+ *				{
+ *					name: 'span',
+ *					class: 'bold'
+ *				},
+ *				{
+ *					name: 'span',
+ *					style: {
+ *						'font-weight': 'bold'
+ *					}
+ *				},
+ *				viewElement => {
+ *					const fontWeight = viewElement.getStyle( 'font-weight' );
+ *
+ *					if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
+ *						return {
+ *							name: true,
+ *							style: [ 'font-weight' ]
+ *						};
+ *					}
+ *				}
+ *			]
+ *		} );
+ *
+ *		attributeToElement( conversion, 'styled', {
+ *			model: 'dark',
+ *			view: {
+ *				name: 'span',
+ *				class: [ 'styled', 'styled-dark' ]
+ *			}
+ *		} );
+ *
+ *		attributeToElement( conversion, 'fontSize', [
+ *			{
+ *				model: 'big',
+ *				view: {
+ *					name: 'span',
+ *					style: {
+ *						'font-size': '1.2em'
+ *					}
+ *				}
+ *			},
+ *			{
+ *				model: 'small',
+ *				view: {
+ *					name: 'span',
+ *					style: {
+ *						'font-size': '0.8em'
+ *					}
+ *				}
+ *			}
+ *		] );
+ *
+ *		attributeToElement( conversion, 'fontSize', [
+ *			{
+ *				model: 'big',
+ *				view: {
+ *					name: 'span',
+ *					style: {
+ *						'font-size': '1.2em'
+ *					}
+ *				},
+ *				upcastAlso: viewElement => {
+ *					const fontSize = viewElement.getStyle( 'font-size' );
+ *
+ *					if ( !fontSize ) {
+ *						return null;
+ *					}
+ *
+ *					const match = fontSize.match( /(\d+)\s*px/ );
+ *
+ *					if ( !match ) {
+ *						return null;
+ *					}
+ *
+ *					const size = Number( match[ 1 ] );
+ *
+ *					if ( viewElement.is( 'span' ) && size > 10 ) {
+ *						return { name: true, style: [ 'font-size' ] };
+ *					}
+ *
+ *					return null;
+ *				}
+ *			},
+ *			{
+ *				model: 'small',
+ *				view: {
+ *					name: 'span',
+ *					style: {
+ *						'font-size': '0.8em'
+ *					}
+ *				},
+ *				upcastAlso: viewElement => {
+ *					const fontSize = viewElement.getStyle( 'font-size' );
+ *
+ *					if ( !fontSize ) {
+ *						return null;
+ *					}
+ *
+ *					const match = fontSize.match( /(\d+)\s*px/ );
+ *
+ *					if ( !match ) {
+ *						return null;
+ *					}
+ *
+ *					const size = Number( match[ 1 ] );
+ *
+ *					if ( viewElement.is( 'span' ) && size < 10 ) {
+ *						return { name: true, style: [ 'font-size' ] };
+ *					}
+ *
+ *					return null;
+ *				}
+ *			}
+ *		] );
+ *
+ * @param {module:engine/conversion/conversion~Conversion} conversion Conversion class instance with registered conversion dispatchers.
+ * @param {String} modelAttributeKey The key of the model attribute to convert.
+ * @param {Object|Array.<Object>} definition Conversion definition. It is possible to provide multiple definitions in an array.
+ * @param {*} [definition.model] The value of the converted model attribute. If omitted, when downcasted, the item will be treated
+ * as a default item, that will be used when no other item matches. When upcasted, the model attribute value will be set to `true`.
+ * @param {module:engine/view/elementdefinition~ElementDefinition} definition.view Definition of a view element to convert from/to.
+ * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
+ * Any view element matching `upcastAlso` will also be converted to the given model element.
+ */
+export function attributeToElement( conversion, modelAttributeKey, definition ) {
+	// Set downcast (model to view conversion).
+	conversion.for( 'downcast' ).add( downcastAttributeToElement( modelAttributeKey, definition ) );
+
+	// Set upcast (view to model conversion). In this case, we need to re-organise the definition config.
+	if ( !Array.isArray( definition ) ) {
+		definition = [ definition ];
+	}
+
+	for ( const item of definition ) {
+		const model = _getModelAttributeDefinition( modelAttributeKey, item.model );
+
+		for ( const view of _getAllViews( item ) ) {
+			const priority = view == item.view ? 'normal' : 'high';
+
+			conversion.for( 'upcast' ).add( upcastElementToAttribute( {
+				view,
+				model
+			}, priority ) );
+		}
+	}
+}
+
+/**
+ * Defines a conversion between the model and the view where a model attribute is represented as a view attribute (and vice versa).
+ * For example, `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>` (same attribute name and value).
+ *
+ *		attributeToAttribute( conversion, 'src' );
+ *
+ *		attributeToAttribute( conversion, 'source', { view: 'src' } );
+ *
+ *		attributeToAttribute( conversion, 'aside', {
+ *			model: true,
+ *			view: {
+ *				name: 'img',
+ *				key: 'class',
+ *				value: 'aside half-size'
+ *			}
+ *		} );
+ *
+ *		attributeToAttribute( conversion, 'styled', [
+ *			{
+ *				model: 'dark',
+ *				view: {
+ *					key: 'class',
+ *					value: 'styled styled-dark'
+ *				}
+ *			},
+ *			{
+ *				model: 'light',
+ *				view: {
+ *					key: 'class',
+ *					value: 'styled styled-light'
+ *				}
+ *			}
+ *		] );
+ *
+ *		attributeToAttribute( conversion, 'align', [
+ *			{
+ *				model: 'right',
+ *				view: {
+ *					key: 'class',
+ *					value: 'align-right'
+ *				},
+ *				upcastAlso: viewElement => {
+ *					if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
+ *						return {
+ *							style: [ 'text-align' ]
+ *						};
+ *					}
+ *
+ *					return null;
+ *				}
+ *			},
+ *			{
+ *				model: 'center',
+ *				view: {
+ *					key: 'class',
+ *					value: 'align-center'
+ *				},
+ *				upcastAlso: {
+ *					style: {
+ *						'text-align': 'center'
+ *					}
+ *				}
+ *			}
+ *		] );
+ *
+ * @param {module:engine/conversion/conversion~Conversion} conversion Conversion class instance with registered conversion dispatchers.
+ * @param {String} modelAttributeKey The key of the model attribute to convert.
+ * @param {Object|Array.<Object>} [definition] Conversion definition. It is possible to provide multiple definitions in an array.
+ * If not set, the conversion helper will assume 1-to-1 conversion, that is the model attribute key and value will be same
+ * as the view attribute key and value.
+ * @param {*} [definition.model] The value of the converted model attribute. If omitted, when downcasting,
+ * the item will be treated as a default item, that will be used when no other item matches. When upcasting conversion,
+ * the model attribute value will be set to the same value as in the view.
+ * @param {Object} definition.view View attribute conversion details. Given object has required `key` property,
+ * specifying view attribute key, optional `value` property, specifying view attribute value and optional `name`
+ * property specifying a view element name from/on which the attribute should be converted. If `value` is not given,
+ * the view attribute value will be equal to model attribute value.
+ * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
+ * Any view element matching `upcastAlso` will also be converted to the given model element.
+ */
+export function attributeToAttribute( conversion, modelAttributeKey, definition ) {
+	// Set up downcast converter.
+	conversion.for( 'downcast' ).add( downcastAttributeToAttribute( modelAttributeKey, definition ) );
+
+	// Set up upcast converter. In this case, we need to re-organise the definition config.
+	if ( !definition ) {
+		definition = { view: modelAttributeKey };
+	}
+
+	if ( !Array.isArray( definition ) ) {
+		definition = [ definition ];
+	}
+
+	for ( const item of definition ) {
+		const model = _getModelAttributeDefinition( modelAttributeKey, item.model );
+
+		for ( const view of _getAllViews( item ) ) {
+			const priority = view == item.view ? 'low' : 'normal';
+
+			conversion.for( 'upcast' ).add( upcastAttributeToAttribute( {
+				view,
+				model
+			}, priority ) );
+		}
+	}
+}
+
+// Helper function, normalizes input data into a correct config form that can be accepted by conversion helpers. The
+// correct form is either `String` or an object with `key` and `value` properties.
+//
+// @param {String} key Model attribute key.
+// @param {*} [model] Model attribute value.
+// @returns {Object} Normalized model attribute definition.
+function _getModelAttributeDefinition( key, model ) {
+	if ( model === undefined ) {
+		return key;
+	} else {
+		return {
+			key, value: model
+		};
+	}
+}
+
+// Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
+// `definition.upcastAlso`.
+//
+// @param {Object} definition Conversion definition.
+// @returns {Array} Array containing view definitions.
+function _getAllViews( definition ) {
+	if ( !definition.upcastAlso ) {
+		return [ definition.view ];
+	} else {
+		const upcastAlso = Array.isArray( definition.upcastAlso ) ? definition.upcastAlso : [ definition.upcastAlso ];
+
+		return [ definition.view ].concat( upcastAlso );
+	}
+}

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

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

+ 1 - 1
packages/ckeditor5-engine/src/conversion/view-selection-to-model-converters.js

@@ -7,7 +7,7 @@
  * Contains {@link module:engine/view/selection~Selection view selection}
  * to {@link module:engine/model/selection~Selection model selection} conversion helpers.
  *
- * @module engine/conversion/view-selection-to-model-converters
+ * @module engine/conversion/upcast-selection-converters
  */
 
 import ModelSelection from '../model/selection';

+ 91 - 75
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module engine/conversion/viewconversiondispatcher
+ * @module engine/conversion/upcastdispatcher
  */
 
 import ViewConsumable from './viewconsumable';
@@ -17,55 +17,31 @@ import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 /**
- * `ViewConversionDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
+ * `UpcastDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
  * converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
  * {@link module:engine/view/element~Element} into another structure.
  * In default application, {@link module:engine/view/view view} is converted to {@link module:engine/model/model}.
  *
  * During conversion process, for all {@link module:engine/view/node~Node view nodes} from the converted view document fragment,
- * `ViewConversionDispatcher` fires corresponding events. Special callbacks called "converters" should listen to
- * `ViewConversionDispatcher` for those events.
+ * `UpcastDispatcher` fires corresponding events. Special callbacks called "converters" should listen to
+ * `UpcastDispatcher` for those events.
  *
- * Each callback, as a first argument, is passed a special object `data` that has `viewItem`, `modelCursor` and
+ * Each callback, as the second argument, is passed a special object `data` that has `viewItem`, `modelCursor` and
  * `modelRange` properties. `viewItem` property contains {@link module:engine/view/node~Node view node} or
  * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
  * that is converted at the moment and might be handled by the callback. `modelRange` property should be used to save the result
  * of conversion and is always a {@link module:engine/model/range~Range} when conversion result is correct.
  * `modelCursor` property is a {@link module:engine/model/position~Position position} on which conversion result will be inserted
  * and is a context according to {@link module:engine/model/schema~Schema schema} will be checked before the conversion.
- * See also {@link ~ViewConversionDispatcher#convert}. It is also shared by reference by all callbacks listening to given event.
+ * See also {@link ~UpcastDispatcher#convert}. It is also shared by reference by all callbacks listening to given event.
  *
- * The third parameter passed to a callback is an instance of {@link ~ViewConversionDispatcher}
+ * The third parameter passed to a callback is an instance of {@link ~UpcastDispatcher}
  * which provides additional tools for converters.
  *
- * Examples of providing callbacks for `ViewConversionDispatcher`:
- *
- *		// Converter for paragraphs (<p>).
- *		viewDispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
- *			// Create paragraph element.
- *			const paragraph = conversionApi.createElement( 'paragraph' );
- *
- *			// Check if paragraph is allowed on current cursor position.
- *			if ( conversionApi.schema.checkChild( data.modelCursor, paragraph ) ) {
- *				// Try to consume value from consumable list.
- *				if ( !conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
- *					// Insert paragraph on cursor position.
- *					conversionApi.writer.insert( paragraph, data.modelCursor );
- *
- *					// Convert paragraph children to paragraph element.
- *					conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( paragraph ) );
- *
- *					// Wrap paragraph in range and set as conversion result.
- *					data.modelRange = ModelRange.createOn( paragraph );
- *
- *					// Continue conversion just after paragraph.
- *					data.modelCursor = data.modelRange.end;
- *				}
- *			}
- *		} );
+ * Examples of providing callbacks for `UpcastDispatcher`:
  *
  *		// Converter for links (<a>).
- *		viewDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
+ *		upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
  *			if ( conversionApi.consumable.consume( data.viewItem, { name: true, attributes: [ 'href' ] } ) ) {
  *				// <a> element is inline and is represented by an attribute in the model.
  *				// This is why we need to convert only children.
@@ -79,12 +55,35 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  *			}
  *		} );
  *
- *		// Fire conversion.
- *		// Always take care where the converted model structure will be appended to. If this `viewDocumentFragment`
- *		// is going to be appended directly to a '$root' element, use that in `context`.
- *		viewDispatcher.convert( viewDocumentFragment, '$root' );
+ *		// Convert all elements which have no custom converter into paragraph (autoparagraphing).
+ *  	data.viewToModel.on( 'element', ( evt, data, conversionApi ) => {
+ *  	 	// When element is already consumed by higher priority converters then do nothing.
+ *  	 	if ( conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
+ *  	 			const paragraph = conversionApi.writer.createElement( 'paragraph' );
+ *
+ *  	 			// Find allowed parent for paragraph that we are going to insert. If current parent does not allow
+ *  	 			// to insert paragraph but one of the ancestors does then split nodes to allowed parent.
+ *  	 			const splitResult = conversionApi.splitToAllowedParent( paragraph, data.modelCursor );
+ *
+ *  	 			// When there is no split result it means that we can't insert paragraph in this position.
+ *  	 			if ( splitResult ) {
+ *  	 				// Insert paragraph in allowed position.
+ *  	 				conversionApi.writer.insert( paragraph, splitResult.position );
+ *
+ *  	 				// Convert children to paragraph.
+ *  	 				const { modelRange } = conversionApi.convertChildren( data.viewItem, Position.createAt( paragraph ) );
+ *
+ * 						// Set as conversion result, attribute converters may use this property.
+ *  	 				data.modelRange = new Range( Position.createBefore( paragraph ), modelRange.end );
  *
- * Before each conversion process, `ViewConversionDispatcher` fires {@link ~ViewConversionDispatcher#event:viewCleanup}
+ *  	 				// Continue conversion inside paragraph.
+ *  	 				data.modelCursor = data.modelRange.end;
+ *  	 			}
+ *  	 		}
+ *  	 	}
+ *  	 }, { priority: 'low' } );
+ *
+ * Before each conversion process, `UpcastDispatcher` fires {@link ~UpcastDispatcher#event:viewCleanup}
  * event which can be used to prepare tree view for conversion.
  *
  * @mixes module:utils/emittermixin~EmitterMixin
@@ -93,14 +92,14 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  * @fires text
  * @fires documentFragment
  */
-export default class ViewConversionDispatcher {
+export default class UpcastDispatcher {
 	/**
-	 * Creates a `ViewConversionDispatcher` that operates using passed API.
+	 * Creates a `UpcastDispatcher` that operates using passed API.
 	 *
-	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi
+	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi
 	 * @param {module:engine/model/model~Model} model Data model.
 	 * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
-	 * by `ViewConversionDispatcher`.
+	 * by `UpcastDispatcher`.
 	 */
 	constructor( model, conversionApi = {} ) {
 		/**
@@ -123,14 +122,9 @@ export default class ViewConversionDispatcher {
 		this._removeIfEmpty = new Set();
 
 		/**
-		 * Position where conversion result will be inserted. Note that it's not exactly position in one of the
-		 * {@link module:engine/model/document~Document#roots document roots} but it's only a similar position.
-		 * At the beginning of conversion process fragment of model tree is created according to given context and this
-		 * position is created in the top element of created fragment. Then {@link module:engine/view/item~Item View items}
-		 * are converted to this position what makes possible to precisely check converted items by
-		 * {@link module:engine/model/schema~Schema}.
-		 *
-		 * After conversion process position is cleared.
+		 * Position in the temporary structure where the converted content is inserted. The structure reflect the context of
+		 * the target position where the content will be inserted. This property is build based on the context parameter of the
+		 * convert method.
 		 *
 		 * @private
 		 * @type {module:engine/model/position~Position|null}
@@ -140,12 +134,12 @@ export default class ViewConversionDispatcher {
 		/**
 		 * Interface passed by dispatcher to the events callbacks.
 		 *
-		 * @member {module:engine/conversion/viewconversiondispatcher~ViewConversionApi}
+		 * @member {module:engine/conversion/upcastdispatcher~ViewConversionApi}
 		 */
 		this.conversionApi = Object.assign( {}, conversionApi );
 
-		// `convertItem`, `convertChildren` and `splitToAllowedParent` are bound to this `ViewConversionDispatcher`
-		// instance and set on `conversionApi`. This way only a part of `ViewConversionDispatcher` API is exposed.
+		// `convertItem`, `convertChildren` and `splitToAllowedParent` are bound to this `UpcastDispatcher`
+		// instance and set on `conversionApi`. This way only a part of `UpcastDispatcher` API is exposed.
 		this.conversionApi.convertItem = this._convertItem.bind( this );
 		this.conversionApi.convertChildren = this._convertChildren.bind( this );
 		this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
@@ -219,7 +213,7 @@ export default class ViewConversionDispatcher {
 
 	/**
 	 * @private
-	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertItem
+	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#convertItem
 	 */
 	_convertItem( viewItem, modelCursor ) {
 		const data = Object.assign( { viewItem, modelCursor, modelRange: null } );
@@ -249,7 +243,7 @@ export default class ViewConversionDispatcher {
 
 	/**
 	 * @private
-	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren
+	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#convertChildren
 	 */
 	_convertChildren( viewItem, modelCursor ) {
 		const modelRange = new ModelRange( modelCursor );
@@ -269,7 +263,7 @@ export default class ViewConversionDispatcher {
 
 	/**
 	 * @private
-	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#splitToAllowedParent
+	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#splitToAllowedParent
 	 */
 	_splitToAllowedParent( node, modelCursor ) {
 		// Try to find allowed parent.
@@ -335,7 +329,7 @@ export default class ViewConversionDispatcher {
 	}
 
 	/**
-	 * Fired before the first conversion event, at the beginning of view to model conversion process.
+	 * Fired before the first conversion event, at the beginning of upcast (view to model conversion) process.
 	 *
 	 * @event viewCleanup
 	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
@@ -350,16 +344,15 @@ export default class ViewConversionDispatcher {
 	 * all elements conversion or to conversion of specific elements.
 	 *
 	 * @event element
-	 * @param {Object} data Object containing viewItem to convert, modelCursor as a conversion position and placeholder
-	 * for modelRange that is a conversion result. Keep in mind that this object is shared by reference between all
+	 * @param {Object} data Conversion data. Keep in mind that this object is shared by reference between all
 	 * callbacks that will be called. This means that callbacks can override values if needed, and those values will
 	 * be available in other callbacks.
 	 * @param {module:engine/view/item~Item} data.viewItem Converted item.
-	 * @param {module:engine/model/position~Position} data.modelCursor Target position for current item.
+	 * @param {module:engine/model/position~Position} data.modelCursor Position where a converter should start changes.
+	 * Change this value for the next converter to tell where the conversion should continue.
 	 * @param {module:engine/model/range~Range} data.modelRange The current state of conversion result. Every change to
 	 * converted element should be reflected by setting or modifying this property.
-	 * @param {ViewConversionApi} conversionApi Conversion interface to be used by callback, passed in
-	 * `ViewConversionDispatcher` constructor.
+	 * @param {ViewConversionApi} conversionApi Conversion utilities to be used by callback.
 	 */
 
 	/**
@@ -377,7 +370,7 @@ export default class ViewConversionDispatcher {
 	 */
 }
 
-mix( ViewConversionDispatcher, EmitterMixin );
+mix( UpcastDispatcher, EmitterMixin );
 
 // Traverses given model item and searches elements which marks marker range. Found element is removed from
 // DocumentFragment but path of this element is stored in a Map which is then returned.
@@ -443,8 +436,8 @@ function createContextTree( contextDefinition, writer ) {
 }
 
 /**
- * Conversion interface that is registered for given {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}
- * and is passed as one of parameters when {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher dispatcher}
+ * Conversion interface that is registered for given {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}
+ * and is passed as one of parameters when {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher dispatcher}
  * fires it's events.
  *
  * @interface ViewConversionApi
@@ -458,9 +451,9 @@ function createContextTree( contextDefinition, writer ) {
  * The `modelRange` must be {@link module:engine/model/range~Range model range} or `null` (as set by default).
  *
  * @method #convertItem
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
  * @param {module:engine/view/item~Item} viewItem Item to convert.
  * @param {module:engine/model/position~Position} modelCursor Position of conversion.
  * @returns {Object} result Conversion result.
@@ -473,9 +466,9 @@ function createContextTree( contextDefinition, writer ) {
  * Starts conversion of all children of given item by firing appropriate events for all those children.
  *
  * @method #convertChildren
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
  * @param {module:engine/view/item~Item} viewItem Item to convert.
  * @param {module:engine/model/position~Position} modelCursor Position of conversion.
  * @returns {Object} result Conversion result.
@@ -485,13 +478,31 @@ function createContextTree( contextDefinition, writer ) {
  */
 
 /**
- * Find allowed parent for element that we are going to insert starting from given position.
- * If current parent does not allow to insert element but one of the ancestors does then split nodes to allowed parent.
+ * Checks {@link module:engine/model/schema~Schema schema} to find allowed parent for element that we are going to insert
+ * starting from given position. If current parent does not allow to insert element but one of the ancestors does then
+ * split nodes to allowed parent.
+ *
+ * If schema allows to insert node in given position, nothing is split and object with that position is returned.
+ *
+ * If it was not possible to find allowed parent, `null` is returned, nothing is split.
+ *
+ * Otherwise, ancestors are split and object with position and the copy of the split element is returned.
+ *
+ * For instance, if `<image>` is not allowed in `<paragraph>` but is allowed in `$root`:
+ *
+ *		<paragraph>foo[]bar</paragraph>
+ *
+ *  	-> split for `<image>` ->
+ *
+ *  	<paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
+ *
+ * In the sample above position between `<paragraph>` elements will be returned as `position` and the second `paragraph`
+ * as `cursorParent`.
  *
  * @method #splitToAllowedParent
  * @param {module:engine/model/position~Position} position Position on which element is going to be inserted.
  * @param {module:engine/model/node~Node} node Node to insert.
- * @returns {Object} Split result.
+ * @returns {Object|null} Split result. If it was not possible to find allowed position `null` is returned.
  * @returns {module:engine/model/position~Position} position between split elements.
  * @returns {module:engine/model/element~Element} [cursorParent] Element inside which cursor should be placed to
  * continue conversion. When element is not defined it means that there was no split.
@@ -506,7 +517,12 @@ function createContextTree( contextDefinition, writer ) {
  */
 
 /**
- * Custom data stored by converter for conversion process.
+ * Custom data stored by converters for conversion process. Custom properties of this object can be defined and use to
+ * pass parameters between converters.
+ *
+ * The difference between this property and `data` parameter of
+ * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element} is that `data` parameters allows you
+ * to pass parameters within a single event and `store` within the whole conversion.
  *
  * @param {Object} #store
  */

+ 0 - 60
packages/ckeditor5-engine/src/conversion/view-to-model-converters.js

@@ -1,60 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Range from '../model/range';
-
-/**
- * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
- * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}.
- *
- * @module engine/conversion/view-to-model-converters
- */
-
-/**
- * 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 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 = Range.createFromPositionAndShift( data.modelCursor, text.offsetSize );
-				data.modelCursor = data.modelRange.end;
-			}
-		}
-	};
-}

+ 42 - 29
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -16,24 +16,24 @@ import Model from '../model/model';
 import Batch from '../model/batch';
 import ModelRange from '../model/range';
 import ModelPosition from '../model/position';
-import ModelConversionDispatcher from '../conversion/modelconversiondispatcher';
+import DowncastDispatcher from '../conversion/downcastdispatcher';
 import ModelSelection from '../model/selection';
 import ModelDocumentFragment from '../model/documentfragment';
 import DocumentSelection from '../model/documentselection';
 
-import ViewConversionDispatcher from '../conversion/viewconversiondispatcher';
-import ViewSelection from '../view/selection';
-import ViewDocumentFragment from '../view/documentfragment';
+import UpcastDispatcher from '../conversion/upcastdispatcher';
+import ViewDocument from '../view/document';
 import ViewContainerElement from '../view/containerelement';
 import ViewAttributeElement from '../view/attributeelement';
+import ViewRootEditableElement from '../view/rooteditableelement';
 
 import Mapper from '../conversion/mapper';
 import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view';
 import {
 	convertRangeSelection,
 	convertCollapsedSelection,
-} from '../conversion/model-selection-to-view-converters';
-import { insertText, insertElement, wrap } from '../conversion/model-to-view-converters';
+} from '../conversion/downcast-selection-converters';
+import { insertText, insertElement, wrap } from '../conversion/downcast-converters';
 import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObject';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 
@@ -190,39 +190,52 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 		selection = new ModelSelection( selectionOrPositionOrRange );
 	}
 
-	// Setup model to view converter.
-	const viewDocumentFragment = new ViewDocumentFragment();
-	const viewSelection = new ViewSelection();
-	const modelToView = new ModelConversionDispatcher( model, { mapper, viewSelection } );
+	// Set up conversion.
+	// Create a temporary view document.
+	const viewDocument = new ViewDocument();
+	const viewRoot = new ViewRootEditableElement( 'div' );
+
+	// Create a temporary root element in view document.
+	viewRoot.document = viewDocument;
+	viewRoot.rootName = 'main';
+	viewDocument.roots.add( viewRoot );
+
+	// Create and setup downcast dispatcher.
+	const downcastDispatcher = new DowncastDispatcher( model, { mapper, viewSelection: viewDocument.selection } );
 
 	// Bind root elements.
-	mapper.bindElements( node.root, viewDocumentFragment );
+	mapper.bindElements( node.root, viewRoot );
 
-	modelToView.on( 'insert:$text', insertText() );
-	modelToView.on( 'attribute', wrap( ( value, data ) => {
+	downcastDispatcher.on( 'insert:$text', insertText() );
+	downcastDispatcher.on( 'attribute', wrap( ( value, data ) => {
 		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( 'textProxy' ) ) {
 			return new ViewAttributeElement( 'model-text-with-attributes', { [ data.attributeKey ]: stringifyAttributeValue( value ) } );
 		}
 	} ) );
-	modelToView.on( 'insert', insertElement( data => {
+	downcastDispatcher.on( 'insert', insertElement( modelItem => {
 		// Stringify object types values for properly display as an output string.
-		const attributes = convertAttributes( data.item.getAttributes(), stringifyAttributeValue );
+		const attributes = convertAttributes( modelItem.getAttributes(), stringifyAttributeValue );
 
-		return new ViewContainerElement( data.item.name, attributes );
+		return new ViewContainerElement( modelItem.name, attributes );
 	} ) );
-	modelToView.on( 'selection', convertRangeSelection() );
-	modelToView.on( 'selection', convertCollapsedSelection() );
+	downcastDispatcher.on( 'selection', convertRangeSelection() );
+	downcastDispatcher.on( 'selection', convertCollapsedSelection() );
 
 	// Convert model to view.
-	modelToView.convertInsert( range );
+	downcastDispatcher.convertInsert( range );
 
 	// Convert model selection to view selection.
 	if ( selection ) {
-		modelToView.convertSelection( selection );
+		downcastDispatcher.convertSelection( selection );
 	}
 
 	// Parse view to data string.
-	const data = viewStringify( viewDocumentFragment, viewSelection, { sameSelectionCharacters: true } );
+	let data = viewStringify( viewRoot, viewDocument.selection, { sameSelectionCharacters: true } );
+
+	// Removing unneccessary <div> and </div> added because `viewRoot` was also stringified alongside input data.
+	data = data.substr( 5, data.length - 11 );
+
+	viewDocument.destroy();
 
 	// Replace valid XML `model-text-with-attributes` element name to `$text`.
 	return data.replace( new RegExp( 'model-text-with-attributes', 'g' ), '$text' );
@@ -269,18 +282,18 @@ export function parse( data, schema, options = {} ) {
 		viewDocumentFragment = parsedResult;
 	}
 
-	// Setup view to model converter.
-	const viewToModel = new ViewConversionDispatcher( new Model(), { schema, mapper } );
+	// Set up upcast dispatcher.
+	const upcastDispatcher = new UpcastDispatcher( new Model(), { schema, mapper } );
 
-	viewToModel.on( 'documentFragment', convertToModelFragment() );
-	viewToModel.on( 'element:model-text-with-attributes', convertToModelText( true ) );
-	viewToModel.on( 'element', convertToModelElement() );
-	viewToModel.on( 'text', convertToModelText() );
+	upcastDispatcher.on( 'documentFragment', convertToModelFragment() );
+	upcastDispatcher.on( 'element:model-text-with-attributes', convertToModelText( true ) );
+	upcastDispatcher.on( 'element', convertToModelElement() );
+	upcastDispatcher.on( 'text', convertToModelText() );
 
-	viewToModel.isDebug = true;
+	upcastDispatcher.isDebug = true;
 
 	// Convert view to model.
-	let model = viewToModel.convert( viewDocumentFragment.root, options.context || '$root' );
+	let model = upcastDispatcher.convert( viewDocumentFragment.root, options.context || '$root' );
 
 	mapper.bindElements( model, viewDocumentFragment.root );
 

+ 14 - 5
packages/ckeditor5-engine/src/model/markercollection.js

@@ -270,11 +270,20 @@ mix( MarkerCollection, EmitterMixin );
  * Since markers need to track change in the document, for efficiency reasons, it is best to create and keep as little
  * markers as possible and remove them as soon as they are not needed anymore.
  *
- * Markers can be converted to view by adding appropriate converters for
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addMarker} and
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:removeMarker}
- * events, or by building converters for {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}
- * using {@link module:engine/conversion/buildmodelconverter~buildModelConverter model converter builder}.
+ * Markers can be downcasted and upcasted.
+ *
+ * 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.
+ * 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
+ * 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
+ * and then trying to find that part of document would require traversing whole document tree. Marker gives instant access
+ * to the range which it is marking at the moment.
+ *
+ * `Marker` instances are created and destroyed only by {@link ~MarkerCollection MarkerCollection}.
  */
 class Marker {
 	/**

+ 25 - 5
packages/ckeditor5-engine/src/view/element.js

@@ -60,11 +60,7 @@ export default class Element extends Node {
 		 * @protected
 		 * @member {Map} #_attrs
 		 */
-		if ( isPlainObject( attrs ) ) {
-			this._attrs = objectToMap( attrs );
-		} else {
-			this._attrs = new Map( attrs );
-		}
+		this._attrs = parseAttributes( attrs );
 
 		/**
 		 * Array of child nodes.
@@ -723,6 +719,30 @@ export default class Element extends Node {
 	 */
 }
 
+// Parses attributes provided to the element constructor before they are applied to an element. If attributes are passed
+// as an object (instead of `Map`), the object is transformed to the map. Attributes with `null` value are removed.
+// Attributes with non-`String` value are converted to `String`.
+//
+// @param {Object|Map} attrs Attributes to parse.
+// @returns {Map} Parsed attributes.
+function parseAttributes( attrs ) {
+	if ( isPlainObject( attrs ) ) {
+		attrs = objectToMap( attrs );
+	} else {
+		attrs = new Map( attrs );
+	}
+
+	for ( const [ key, value ] of attrs ) {
+		if ( value === null ) {
+			attrs.delete( key );
+		} else if ( typeof value != 'string' ) {
+			attrs.set( key, String( value ) );
+		}
+	}
+
+	return attrs;
+}
+
 // Parses inline styles and puts property - value pairs into styles map.
 // Styles map is cleared before insertion.
 //

+ 58 - 0
packages/ckeditor5-engine/src/view/elementdefinition.jsdoc

@@ -0,0 +1,58 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/view/elementdefinition
+ */
+
+/**
+ * A plain object that describes a view element in a way that a concrete, exact view element could be created from that description.
+ *
+ *		const viewDefinition = {
+ *			name: 'h1',
+ *			class: [ 'foo', 'bar' ]
+ *		};
+ *
+ * Above describes a view element:
+ *
+ *		<h1 class="foo bar"></h1>
+ *
+ * An example with styles and attributes:
+ *
+ *      const viewDefinition = {
+ *          name: 'span',
+ *          style: {
+ *              'font-size': '12px',
+ *              'font-weight': 'bold'
+ *          },
+ *          attribute: {
+ *              'data-id': '123'
+ *          }
+ *      };
+ *
+ * Describes:
+ *
+ *      <span style="font-size:12px;font-weight:bold" data-id="123"></span>
+ *
+ * Elements without attributes can be given simply as a string:
+ *
+ *		const viewDefinition = 'p';
+ *
+ * Which will be treated as:
+ *
+ *		const viewDefinition = {
+ *			name: 'p'
+ *		};
+ *
+ * @typedef {String|Object} module:engine/view/elementdefinition~ElementDefinition
+ *
+ * @property {String} name View element name.
+ * @property {String|Array.<String>} [class] Class name or array of class names to match. Each name can be
+ * provided in a form of string.
+ * @property {Object} [style] Object with key-value pairs representing styles. Each object key represents style name.
+ * Value under that key must be a string.
+ * @property {Object} [attribute] Object with key-value pairs representing attributes. Each object key represents
+ * attribute name. Value under that key must be a string.
+ */

+ 103 - 46
packages/ckeditor5-engine/src/view/matcher.js

@@ -27,63 +27,18 @@ export default class Matcher {
 	/**
 	 * Adds pattern or patterns to matcher instance.
 	 *
-	 * Example patterns matching element's name:
-	 *
 	 *		// String.
 	 *		matcher.add( 'div' );
-	 *		matcher.add( { name: 'div' } );
 	 *
 	 *		// Regular expression.
 	 *		matcher.add( /^\w/ );
-	 *		matcher.add( { name: /^\w/ } );
-	 *
-	 * Example pattern matching element's attributes:
-	 *
-	 *		matcher.add( {
-	 *			attribute: {
-	 *				title: 'foobar',	// Attribute title should equal 'foobar'.
-	 *				foo: /^\w+/,		// Attribute foo should match /^\w+/ regexp.
-	 *				bar: true			// Attribute bar should be set (can be empty).
-	 *			}
-	 *		} );
-	 *
-	 * Example patterns matching element's classes:
 	 *
 	 *		// Single class.
 	 *		matcher.add( {
 	 *			class: 'foobar'
 	 *		} );
 	 *
-	 *		// Single class using regular expression.
-	 *		matcher.add( {
-	 *			class: /foo.../
-	 *		} );
-	 *
-	 *		// Multiple classes to match.
-	 *		matcher.add( {
-	 *			class: [ 'baz', 'bar', /foo.../ ]
-	 *		} ):
-	 *
-	 * Example pattern matching element's styles:
-	 *
-	 *		matcher.add( {
-	 *			style: {
-	 *				position: 'absolute',
-	 *				color: /^\w*blue$/
-	 *			}
-	 *		} );
-	 *
-	 * Example function pattern:
-	 *
-	 *		matcher.add( ( element ) => {
-	 *			// Result of this function will be included in `match`
-	 *			// property of the object returned from matcher.match() call.
-	 *			if ( element.name === 'div' && element.childCount > 0 ) {
-	 *				return { name: true };
-	 *			}
-	 *
-	 *			return null;
-	 *		} );
+	 * See {@link module:engine/view/matcher~MatcherPattern} for more examples.
 	 *
 	 * Multiple patterns can be added in one call:
 	 *
@@ -383,3 +338,105 @@ function matchStyles( patterns, element ) {
 
 	return match;
 }
+
+/**
+ * An entity that is a valid pattern recognized by a matcher. `MatcherPattern` is used by {@link ~Matcher} to recognize
+ * if a view element fits in a group of view elements described by the pattern.
+ *
+ * `MatcherPattern` can be given as a `String`, a `RegExp`, an `Object` or a `Function`.
+ *
+ * If `MatcherPattern` is given as a `String` or `RegExp`, it will match any view element that has a matching name:
+ *
+ *		// Match any element with name equal to 'div'.
+ *		const pattern = 'div';
+ *
+ *		// Match any element which name starts on 'p'.
+ *		const pattern = /^p/;
+ *
+ * If `MatcherPattern` is given as an `Object`, all the object's properties will be matched with view element properties.
+ *
+ *		// Match view element's name.
+ *		const pattern = { name: /^p/ };
+ *
+ *		// Match view element which has matching attributes.
+ *		const pattern = {
+ *			attribute: {
+ *				title: 'foobar',	// Attribute title should equal 'foobar'.
+ *				foo: /^\w+/,		// Attribute foo should match /^\w+/ regexp.
+ *				bar: true			// Attribute bar should be set (can be empty).
+ *			}
+ *		};
+ *
+ *		// Match view element which has given class.
+ *		const pattern = {
+ *			class: 'foobar'
+ *		};
+ *
+ *		// Match view element class using regular expression.
+ *		const pattern = {
+ *			class: /foo.../
+ *		};
+ *
+ *		// Multiple classes to match.
+ *		const pattern = {
+ *			class: [ 'baz', 'bar', /foo.../ ]
+ *		}:
+ *
+ *		// Match view element which has given styles.
+ *		const pattern = {
+ *			style: {
+ *				position: 'absolute',
+ *				color: /^\w*blue$/
+ *			}
+ *		};
+ *
+ *		// Pattern with multiple properties.
+ *		const pattern = {
+ *			name: 'span',
+ *			style: {
+ *				'font-weight': 'bold'
+ *			},
+ *			class: 'highlighted'
+ *		};
+ *
+ * If `MatcherPattern` is given as a `Function`, the function takes a view element as a first and only parameter and
+ * the function should decide whether that element matches. If so, it should return what part of the view element has been matched.
+ * Otherwise, the function should return `null`. The returned result will be included in `match` property of the object
+ * returned by {@link ~Matcher#match} call.
+ *
+ *		// Match an empty <div> element.
+ *		const pattern = element => {
+ *			if ( element.name == 'div' && element.childCount > 0 ) {
+ *				// Return which part of the element was matched.
+ *				return { name: true };
+ *			}
+ *
+ *			return null;
+ *		};
+ *
+ *		// Match a <p> element with big font ("heading-like" element).
+ *		const pattern = element => {
+ *			if ( element.name == 'p' ) {
+ *				const fontSize = element.getStyle( 'font-size' );
+ *				const size = fontSize.match( /(\d+)/px );
+ *
+ *				if ( size && Number( size[ 1 ] ) > 26 ) {
+ *					return { name: true, attribute: [ 'font-size' ] };
+ *				}
+ *			}
+ *
+ *			return null;
+ *		};
+ *
+ * `MatcherPattern` is defined in a way that it is a superset of {@link module:engine/view/elementdefinition~ElementDefinition},
+ * that is, every `ElementDefinition` also can be used as a `MatcherPattern`.
+ *
+ * @typedef {String|RegExp|Object|Function} module:engine/view/matcher~MatcherPattern
+ *
+ * @property {String|RegExp} [name] View element name to match.
+ * @property {String|RegExp|Array.<String|RegExp>} [class] View element's class name(s) to match.
+ * @property {Object} [style] Object with key-value pairs representing styles to match.
+ * Each object key represents style name. Value can be given as `String` or `RegExp`.
+ * @property {Object} [attribute] Object with key-value pairs representing attributes to match.
+ * Each object key represents attribute name. Value can be given as `String` or `RegExp`.
+ */

+ 0 - 44
packages/ckeditor5-engine/src/view/viewelementdefinition.jsdoc

@@ -1,44 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/view/viewelementdefinition
- */
-
-/**
- * An object defining view element used in {@link module:engine/conversion/definition-based-converters} as part of
- * {@link module:engine/conversion/definition-based-converters~ConverterDefinition}.
- *
- * It describe a view element that is used
- *
- *		const viewDefinition = {
- *			name: 'h1',
- *			class: [ 'foo', 'bar' ]
- *		};
- *
- * Above describes a view element:
- *
- *		<h1 class="foo bar">...</h1>
- *
- * For elements without attributes you can use shorthand string version:
- *
- *		const viewDefinition = 'p';
- *
- * which will be treated as:
- *
- *		const viewDefinition = {
- *			name: 'p'
- *		};
- *
- * @typedef {String|Object} module:engine/view/viewelementdefinition~ViewElementDefinition
- *
- * @property {String} name View element name.
- * @property {String|Array.<String>} [class] Class name or array of class names to match. Each name can be
- * provided in a form of string.
- * @property {Object} [style] Object with key-value pairs representing styles to match. Each object key
- * represents style name. Value under that key must be a string.
- * @property {Object} [attribute] Object with key-value pairs representing attributes to match. Each object key
- * represents attribute name. Value under that key must be a string.
- */

+ 4 - 0
packages/ckeditor5-engine/src/view/writer.js

@@ -456,6 +456,7 @@ export function move( sourceRange, targetPosition ) {
 
 /**
  * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
+ * If a collapsed range is provided, it will be wrapped only if it is equal to view selection.
  *
  * If `viewSelection` was set and a collapsed range was passed, if the range is same as selection, the selection
  * will be moved to the inside of the wrapped attribute element.
@@ -467,6 +468,9 @@ export function move( sourceRange, targetPosition ) {
  * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
  * an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
  *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
+ * is collapsed and different than view selection.
+ *
  * @function module:engine/view/writer~writer.wrap
  * @param {module:engine/view/range~Range} range Range to wrap.
  * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.

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

@@ -8,11 +8,7 @@ import Range from '../../src/model/range';
 import DataController from '../../src/controller/datacontroller';
 import HtmlDataProcessor from '../../src/dataprocessor/htmldataprocessor';
 
-import buildViewConverter from '../../src/conversion/buildviewconverter';
-import buildModelConverter from '../../src/conversion/buildmodelconverter';
-
 import ModelDocumentFragment from '../../src/model/documentfragment';
-
 import ViewDocumentFragment from '../../src/view/documentfragment';
 
 import { getData, setData, stringify, parse as parseModel } from '../../src/dev-utils/model';
@@ -20,6 +16,17 @@ 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';
+
 describe( 'DataController', () => {
 	let model, modelDocument, htmlDataProcessor, data, schema;
 
@@ -59,7 +66,7 @@ describe( 'DataController', () => {
 		it( 'should set paragraph', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
-			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
+			upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher );
 
 			const output = data.parse( '<p>foo<b>bar</b></p>' );
 
@@ -70,7 +77,7 @@ describe( 'DataController', () => {
 		it( 'should set two paragraphs', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
-			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
+			upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher );
 
 			const output = data.parse( '<p>foo</p><p>bar</p>' );
 
@@ -84,10 +91,10 @@ describe( 'DataController', () => {
 				allowAttributes: [ 'bold' ]
 			} );
 
-			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
-			buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', true );
+			upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher );
+			upcastElementToAttribute( { view: 'strong', model: 'bold' } )( data.upcastDispatcher );
 
-			const output = data.parse( '<p>foo<b>bar</b></p>' );
+			const output = data.parse( '<p>foo<strong>bar</strong></p>' );
 
 			expect( output ).to.instanceof( ModelDocumentFragment );
 			expect( stringify( output ) ).to.equal( '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
@@ -110,7 +117,7 @@ describe( 'DataController', () => {
 		beforeEach( () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
-			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
+			upcastElementToElement( { view: 'p', model: 'paragraph' } )( data.upcastDispatcher );
 		} );
 
 		it( 'should convert content of an element #1', () => {
@@ -213,7 +220,7 @@ describe( 'DataController', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo</paragraph>' );
 
-			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
+			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
 
 			expect( data.get() ).to.equal( '<p>foo</p>' );
 		} );
@@ -222,7 +229,7 @@ describe( 'DataController', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph></paragraph>' );
 
-			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
+			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
 
 			expect( data.get() ).to.equal( '<p>&nbsp;</p>' );
 		} );
@@ -231,7 +238,7 @@ describe( 'DataController', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
 
-			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
+			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
 
 			expect( data.get() ).to.equal( '<p>foo</p><p>bar</p>' );
 		} );
@@ -247,7 +254,7 @@ describe( 'DataController', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
-			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
+			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
 
 			expect( data.get() ).to.equal( '<p>foobar</p>' );
 		} );
@@ -256,10 +263,10 @@ describe( 'DataController', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			setData( model, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
-			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
-			buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
+			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
+			downcastAttributeToElement( 'bold', { view: 'strong' } )( data.downcastDispatcher );
 
-			expect( data.get() ).to.equal( '<p>foo<b>bar</b></p>' );
+			expect( data.get() ).to.equal( '<p>foo<strong>bar</strong></p>' );
 		} );
 
 		it( 'should get root name as a parameter', () => {
@@ -269,8 +276,8 @@ describe( 'DataController', () => {
 			setData( model, '<paragraph>foo</paragraph>', { rootName: 'main' } );
 			setData( model, 'Bar', { rootName: 'title' } );
 
-			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
-			buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
+			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
+			downcastAttributeToElement( 'bold', { view: 'strong' } )( data.downcastDispatcher );
 
 			expect( data.get() ).to.equal( '<p>foo</p>' );
 			expect( data.get( 'main' ) ).to.equal( '<p>foo</p>' );
@@ -286,7 +293,7 @@ describe( 'DataController', () => {
 			schema.extend( '$block', { allowIn: 'div' } );
 			schema.extend( 'div', { allowIn: '$root' } );
 
-			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
+			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
 		} );
 
 		it( 'should stringify a content of an element', () => {
@@ -310,7 +317,7 @@ describe( 'DataController', () => {
 			schema.extend( '$block', { allowIn: 'div' } );
 			schema.extend( 'div', { allowIn: '$root' } );
 
-			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
+			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
 		} );
 
 		it( 'should convert a content of an element', () => {
@@ -331,7 +338,7 @@ describe( 'DataController', () => {
 			const modelElement = parseModel( '<div><paragraph>foobar</paragraph></div>', schema );
 			const modelRoot = model.document.getRoot();
 
-			buildModelConverter().for( data.modelToView ).fromMarker( 'marker:a' ).toHighlight( { class: 'a' } );
+			downcastMarkerToHighlight( { model: 'marker:a', view: { class: 'a' } } )( data.downcastDispatcher );
 
 			model.change( writer => {
 				writer.insert( modelElement, modelRoot, 0 );
@@ -348,8 +355,8 @@ describe( 'DataController', () => {
 			const modelElement = parseModel( '<div><paragraph>foo</paragraph><paragraph>bar</paragraph></div>', schema );
 			const modelRoot = model.document.getRoot();
 
-			buildModelConverter().for( data.modelToView ).fromMarker( 'marker:a' ).toHighlight( { class: 'a' } );
-			buildModelConverter().for( data.modelToView ).fromMarker( 'marker:b' ).toHighlight( { class: 'b' } );
+			downcastMarkerToHighlight( { model: 'marker:a', view: { class: 'a' } } )( data.downcastDispatcher );
+			downcastMarkerToHighlight( { model: 'marker:b', view: { class: 'b' } } )( data.downcastDispatcher );
 
 			const modelP1 = modelElement.getChild( 0 );
 			const modelP2 = modelElement.getChild( 1 );

+ 14 - 11
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -12,8 +12,9 @@ import EditingController from '../../src/controller/editingcontroller';
 import ViewDocument from '../../src/view/document';
 
 import Mapper from '../../src/conversion/mapper';
-import ModelConversionDispatcher from '../../src/conversion/modelconversiondispatcher';
-import buildModelConverter from '../../src/conversion/buildmodelconverter';
+import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
+
+import { downcastElementToElement, downcastMarkerToHighlight } from '../../src/conversion/downcast-converters';
 
 import Model from '../../src/model/model';
 import ModelPosition from '../../src/model/position';
@@ -40,7 +41,7 @@ describe( 'EditingController', () => {
 			expect( editing ).to.have.property( 'model' ).that.equals( model );
 			expect( editing ).to.have.property( 'view' ).that.is.instanceof( ViewDocument );
 			expect( editing ).to.have.property( 'mapper' ).that.is.instanceof( Mapper );
-			expect( editing ).to.have.property( 'modelToView' ).that.is.instanceof( ModelConversionDispatcher );
+			expect( editing ).to.have.property( 'downcastDispatcher' ).that.is.instanceof( DowncastDispatcher );
 
 			editing.destroy();
 		} );
@@ -89,9 +90,10 @@ describe( 'EditingController', () => {
 
 			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			model.schema.register( 'div', { inheritAllFrom: '$block' } );
-			buildModelConverter().for( editing.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
-			buildModelConverter().for( editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
-			buildModelConverter().for( editing.modelToView ).fromMarker( 'marker' ).toHighlight( {} );
+
+			downcastElementToElement( { model: 'paragraph', view: 'p' } )( editing.downcastDispatcher );
+			downcastElementToElement( { model: 'div', view: 'div' } )( editing.downcastDispatcher );
+			downcastMarkerToHighlight( { model: 'marker', view: {} } )( editing.downcastDispatcher );
 
 			// Note: The below code is highly overcomplicated due to #455.
 			model.change( writer => {
@@ -355,9 +357,10 @@ describe( 'EditingController', () => {
 
 			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			model.schema.register( 'div', { inheritAllFrom: '$block' } );
-			buildModelConverter().for( editing.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
-			buildModelConverter().for( editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
-			buildModelConverter().for( editing.modelToView ).fromMarker( 'marker' ).toHighlight( {} );
+
+			downcastElementToElement( { model: 'paragraph', view: 'p' } )( editing.downcastDispatcher );
+			downcastElementToElement( { model: 'div', view: 'div' } )( editing.downcastDispatcher );
+			downcastMarkerToHighlight( { model: 'marker', view: {} } )( editing.downcastDispatcher );
 
 			const modelData = new ModelDocumentFragment( parse(
 				'<paragraph>foo</paragraph>' +
@@ -372,7 +375,7 @@ describe( 'EditingController', () => {
 				writer.setSelection( ModelRange.createFromParentsAndOffsets( p1, 0, p1, 0 ) );
 			} );
 
-			mcd = editing.modelToView;
+			mcd = editing.downcastDispatcher;
 			sinon.spy( mcd, 'convertMarkerRemove' );
 		} );
 
@@ -568,7 +571,7 @@ describe( 'EditingController', () => {
 
 			const spy = sinon.spy();
 
-			editing.modelToView.on( 'insert:$element', spy );
+			editing.downcastDispatcher.on( 'insert:$element', spy );
 
 			editing.destroy();
 

+ 0 - 604
packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js

@@ -1,604 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import buildModelConverter from '../../src/conversion/buildmodelconverter';
-
-import Model from '../../src/model/model';
-import ModelElement from '../../src/model/element';
-import ModelText from '../../src/model/text';
-import ModelRange from '../../src/model/range';
-import ModelPosition from '../../src/model/position';
-
-import ViewElement from '../../src/view/element';
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewAttributeElement from '../../src/view/attributeelement';
-import ViewUIElement from '../../src/view/uielement';
-import ViewText from '../../src/view/text';
-
-import EditingController from '../../src/controller/editingcontroller';
-
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
-function viewAttributesToString( item ) {
-	let result = '';
-
-	for ( const key of item.getAttributeKeys() ) {
-		const value = item.getAttribute( key );
-
-		if ( value ) {
-			result += ' ' + key + '="' + value + '"';
-		}
-	}
-
-	return result;
-}
-
-function viewToString( item ) {
-	let result = '';
-
-	if ( item instanceof ViewText ) {
-		result = item.data;
-	} else {
-		// ViewElement or ViewDocumentFragment.
-		for ( const child of item.getChildren() ) {
-			result += viewToString( child );
-		}
-
-		if ( item instanceof ViewElement ) {
-			result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
-		}
-	}
-
-	return result;
-}
-
-describe( 'Model converter builder', () => {
-	let dispatcher, modelDoc, modelRoot, viewRoot, viewSelection, model, controller;
-
-	beforeEach( () => {
-		model = new Model();
-		modelDoc = model.document;
-		modelRoot = modelDoc.createRoot();
-
-		controller = new EditingController( model );
-
-		// Set name of view root the same as dom root.
-		// This is a mock of attaching view root to dom root.
-		controller.view.getRoot()._name = 'div';
-
-		dispatcher = controller.modelToView;
-
-		viewRoot = controller.view.getRoot();
-		viewSelection = controller.view.selection;
-
-		buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
-	} );
-
-	describe( 'model element to view element conversion', () => {
-		it( 'using passed view element name', () => {
-			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-		} );
-
-		it( 'using passed view element', () => {
-			buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( new ViewContainerElement( 'img' ) );
-
-			const modelElement = new ModelElement( 'image' );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><img></img></div>' );
-		} );
-
-		it( 'using passed creator function', () => {
-			buildModelConverter().for( dispatcher )
-				.fromElement( 'header' )
-				.toElement( data => new ViewContainerElement( 'h' + data.item.getAttribute( 'level' ) ) );
-
-			const modelElement = new ModelElement( 'header', { level: 2 }, new ModelText( 'foobar' ) );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><h2>foobar</h2></div>' );
-		} );
-	} );
-
-	describe( 'model attribute to view element conversion', () => {
-		it( 'using passed view element name', () => {
-			buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
-
-			const modelElement = new ModelText( 'foo', { bold: true } );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><strong>foo</strong></div>' );
-
-			model.change( writer => {
-				writer.removeAttribute( 'bold', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
-		} );
-
-		it( 'using passed view element', () => {
-			buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( new ViewAttributeElement( 'strong' ) );
-
-			const modelElement = new ModelText( 'foo', { bold: true } );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><strong>foo</strong></div>' );
-
-			model.change( writer => {
-				writer.removeAttribute( 'bold', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
-		} );
-
-		it( 'using passed creator function', () => {
-			buildModelConverter().for( dispatcher ).fromAttribute( 'italic' ).toElement( value => new ViewAttributeElement( value ) );
-
-			const modelElement = new ModelText( 'foo', { italic: 'em' } );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );
-
-			model.change( writer => {
-				writer.setAttribute( 'italic', 'i', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><i>foo</i></div>' );
-
-			model.change( writer => {
-				writer.removeAttribute( 'italic', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
-		} );
-
-		it( 'selection conversion', () => {
-			// Model converter builder should add selection converter.
-			buildModelConverter().for( dispatcher ).fromAttribute( 'italic' ).toElement( value => new ViewAttributeElement( value ) );
-
-			model.change( writer => {
-				writer.insert( new ModelText( 'foo', { italic: 'em' } ), ModelPosition.createAt( modelRoot, 0 ) );
-
-				// Set collapsed selection after "f".
-				const position = new ModelPosition( modelRoot, [ 1 ] );
-				writer.setSelection( new ModelRange( position, position ) );
-			} );
-
-			// Check if view structure is ok.
-			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );
-
-			// Check if view selection is collapsed after "f" letter.
-			let ranges = Array.from( viewSelection.getRanges() );
-			expect( ranges.length ).to.equal( 1 );
-			expect( ranges[ 0 ].start.isEqual( ranges[ 0 ].end ) ).to.be.true;
-			expect( ranges[ 0 ].start.parent ).to.be.instanceof( ViewText ); // "foo".
-			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
-
-			// Change selection attribute, convert it.
-			model.change( writer => {
-				writer.setSelectionAttribute( 'italic', 'i' );
-			} );
-
-			// Check if view structure has changed.
-			expect( viewToString( viewRoot ) ).to.equal( '<div><em>f</em><i></i><em>oo</em></div>' );
-
-			// Check if view selection is inside new <em> element.
-			ranges = Array.from( viewSelection.getRanges() );
-			expect( ranges.length ).to.equal( 1 );
-			expect( ranges[ 0 ].start.isEqual( ranges[ 0 ].end ) ).to.be.true;
-			expect( ranges[ 0 ].start.parent.name ).to.equal( 'i' );
-			expect( ranges[ 0 ].start.offset ).to.equal( 0 );
-
-			// Some more tests checking how selection attributes changes are converted:
-			model.change( writer => {
-				writer.removeSelectionAttribute( 'italic' );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><em>f</em><em>oo</em></div>' );
-			ranges = Array.from( viewSelection.getRanges() );
-			expect( ranges[ 0 ].start.parent.name ).to.equal( 'div' );
-			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
-
-			model.change( writer => {
-				writer.setSelectionAttribute( 'italic', 'em' );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );
-			ranges = Array.from( viewSelection.getRanges() );
-			expect( ranges.length ).to.equal( 1 );
-			expect( ranges[ 0 ].start.isEqual( ranges[ 0 ].end ) ).to.be.true;
-			expect( ranges[ 0 ].start.parent ).to.be.instanceof( ViewText ); // "foo".
-			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
-		} );
-	} );
-
-	describe( 'model attribute to view attribute conversion', () => {
-		it( 'using default 1-to-1 conversion', () => {
-			buildModelConverter().for( dispatcher ).fromAttribute( 'class' ).toAttribute();
-
-			const modelElement = new ModelElement( 'paragraph', { class: 'myClass' }, new ModelText( 'foobar' ) );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="myClass">foobar</p></div>' );
-
-			model.change( writer => {
-				writer.setAttribute( 'class', 'newClass', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="newClass">foobar</p></div>' );
-
-			model.change( writer => {
-				writer.removeAttribute( 'class', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-		} );
-
-		it( 'using passed attribute key', () => {
-			buildModelConverter().for( dispatcher ).fromAttribute( 'theme' ).toAttribute( 'class' );
-
-			const modelElement = new ModelElement( 'paragraph', { theme: 'abc' }, new ModelText( 'foobar' ) );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="abc">foobar</p></div>' );
-
-			model.change( writer => {
-				writer.setAttribute( 'theme', 'xyz', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="xyz">foobar</p></div>' );
-
-			model.change( writer => {
-				writer.removeAttribute( 'theme', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-		} );
-
-		it( 'using passed attribute key and value', () => {
-			buildModelConverter().for( dispatcher ).fromAttribute( 'highlighted' ).toAttribute( 'style', 'background:yellow' );
-
-			const modelElement = new ModelElement( 'paragraph', { 'highlighted': true }, new ModelText( 'foobar' ) );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p style="background:yellow;">foobar</p></div>' );
-
-			model.change( writer => {
-				writer.removeAttribute( 'highlighted', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-		} );
-
-		it( 'using passed attribute creator function', () => {
-			buildModelConverter().for( dispatcher )
-				.fromAttribute( 'theme' )
-				.toAttribute( value => ( { key: 'class', value: value + '-theme' } ) );
-
-			const modelElement = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="nice-theme">foobar</p></div>' );
-
-			model.change( writer => {
-				writer.setAttribute( 'theme', 'good', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="good-theme">foobar</p></div>' );
-
-			model.change( writer => {
-				writer.removeAttribute( 'theme', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-		} );
-	} );
-
-	describe( 'model marker to highlight converter', () => {
-		let modelText, modelElement;
-
-		beforeEach( () => {
-			modelText = new ModelText( 'foobar' );
-			modelElement = new ModelElement( 'paragraph', null, [ modelText ] );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-		} );
-
-		it( 'using passed highlight descriptor object', () => {
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( {
-				class: 'highlight',
-				priority: 3,
-				attributes: { title: 'highlight title' }
-			} );
-
-			model.change( writer => {
-				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal(
-				'<div>' +
-					'<p>' +
-						'fo' +
-						'<span class="highlight" title="highlight title">ob</span>' +
-						'ar' +
-					'</p>' +
-				'</div>' );
-
-			expect( viewRoot.getChild( 0 ).getChild( 1 ).priority ).to.equal( 3 );
-
-			model.change( writer => {
-				writer.removeMarker( 'search' );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-		} );
-
-		it( 'using passed highlight descriptor object creator', () => {
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( () => ( {
-				class: 'highlight',
-				priority: 12,
-				attributes: { title: 'highlight title' }
-			} ) );
-
-			model.change( writer => {
-				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal(
-				'<div>' +
-					'<p>' +
-						'fo' +
-						'<span class="highlight" title="highlight title">ob</span>' +
-						'ar' +
-					'</p>' +
-				'</div>' );
-
-			expect( viewRoot.getChild( 0 ).getChild( 1 ).priority ).to.equal( 12 );
-
-			model.change( writer => {
-				writer.removeMarker( 'search' );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-		} );
-
-		it( 'should do nothing when marker range is collapsed', () => {
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( {
-				class: 'highlight'
-			} );
-
-			model.change( writer => {
-				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 2 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-
-			model.change( writer => {
-				writer.removeMarker( 'search' );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-		} );
-
-		it( 'should create converters with provided priority', () => {
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( { class: 'highlight' } );
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).withPriority( 'high' ).toHighlight( { class: 'override' } );
-
-			model.change( writer => {
-				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal(
-				'<div>' +
-					'<p>' +
-						'fo' +
-						'<span class="override">ob</span>' +
-						'ar' +
-					'</p>' +
-				'</div>' );
-		} );
-
-		it( 'should throw if trying to convert from attribute', () => {
-			expect( () => {
-				buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toHighlight( { class: 'foo' } );
-			} ).to.throw( CKEditorError, /^build-model-converter-non-marker-to-highlight/ );
-		} );
-
-		it( 'should throw if trying to convert from element', () => {
-			expect( () => {
-				buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toHighlight( { class: 'foo' } );
-			} ).to.throw( CKEditorError, /^build-model-converter-non-marker-to-highlight/ );
-		} );
-	} );
-
-	describe( 'model marker to view element conversion', () => {
-		let modelText, modelElement, range;
-
-		beforeEach( () => {
-			modelText = new ModelText( 'foobar' );
-			modelElement = new ModelElement( 'paragraph', null, [ modelText ] );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-		} );
-
-		describe( 'collapsed range', () => {
-			beforeEach( () => {
-				range = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 2 );
-			} );
-
-			it( 'using passed view element name', () => {
-				buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
-
-				model.change( writer => {
-					writer.setMarker( 'search', range );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span></span>obar</p></div>' );
-
-				model.change( writer => {
-					writer.removeMarker( 'search' );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-			} );
-
-			it( 'using passed view element', () => {
-				const viewElement = new ViewUIElement( 'span', { class: 'search' } );
-				buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( viewElement );
-
-				model.change( writer => {
-					writer.setMarker( 'search', range );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span class="search"></span>obar</p></div>' );
-
-				model.change( writer => {
-					writer.removeMarker( 'search' );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-			} );
-
-			it( 'using passed creator function', () => {
-				buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( data => {
-					const className = 'search search-color-' + data.markerName.split( ':' )[ 1 ];
-
-					return new ViewUIElement( 'span', { class: className } );
-				} );
-
-				model.change( writer => {
-					writer.setMarker( 'search:red', range );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span class="search search-color-red"></span>obar</p></div>' );
-
-				model.change( writer => {
-					writer.removeMarker( 'search:red' );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-			} );
-		} );
-
-		describe( 'non-collapsed range', () => {
-			beforeEach( () => {
-				range = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 );
-			} );
-
-			it( 'using passed view element name', () => {
-				buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
-
-				model.change( writer => {
-					writer.setMarker( 'search', range );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span></span>ob<span></span>ar</p></div>' );
-
-				model.change( writer => {
-					writer.removeMarker( 'search' );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-			} );
-
-			it( 'using passed view element', () => {
-				const viewElement = new ViewUIElement( 'span', { class: 'search' } );
-				buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( viewElement );
-
-				model.change( writer => {
-					writer.setMarker( 'search', range );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal(
-					'<div><p>fo<span class="search"></span>ob<span class="search"></span>ar</p></div>'
-				);
-
-				model.change( writer => {
-					writer.removeMarker( 'search' );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-			} );
-
-			it( 'using passed creator function', () => {
-				buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( data => {
-					const className = 'search search-color-' + data.markerName.split( ':' )[ 1 ];
-
-					return new ViewUIElement( 'span', { class: className } );
-				} );
-
-				model.change( writer => {
-					writer.setMarker( 'search:red', range );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal(
-					'<div><p>fo<span class="search search-color-red"></span>ob<span class="search search-color-red"></span>ar</p></div>'
-				);
-
-				model.change( writer => {
-					writer.removeMarker( 'search:red' );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-			} );
-		} );
-
-		it( 'should overwrite default priority', () => {
-			range = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 2 );
-
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'normal' );
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).withPriority( 'high' ).toElement( 'high' );
-
-			model.change( writer => {
-				writer.setMarker( 'search', range );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<high></high>obar</p></div>' );
-		} );
-
-		it( 'should throw when trying to build model element to view attribute converter', () => {
-			expect( () => {
-				buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toAttribute( 'paragraph', true );
-			} ).to.throw( CKEditorError, /^build-model-converter-non-attribute-to-attribute/ );
-		} );
-	} );
-} );

+ 0 - 631
packages/ckeditor5-engine/tests/conversion/buildviewconverter.js

@@ -1,631 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import buildViewConverter from '../../src/conversion/buildviewconverter';
-
-import Model from '../../src/model/model';
-import ModelDocumentFragment from '../../src/model/documentfragment';
-import ModelElement from '../../src/model/element';
-import ModelTextProxy from '../../src/model/textproxy';
-import ModelRange from '../../src/model/range';
-import ModelWalker from '../../src/model/treewalker';
-
-import ViewDocumentFragment from '../../src/view/documentfragment';
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewAttributeElement from '../../src/view/attributeelement';
-import ViewText from '../../src/view/text';
-import ViewMatcher from '../../src/view/matcher';
-
-import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
-
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
-import { convertToModelFragment, convertText } from '../../src/conversion/view-to-model-converters';
-
-function modelAttributesToString( item ) {
-	let result = '';
-
-	for ( const attr of item.getAttributes() ) {
-		result += ' ' + attr[ 0 ] + '="' + attr[ 1 ] + '"';
-	}
-
-	return result;
-}
-
-function modelToString( item ) {
-	let result = '';
-
-	if ( item instanceof ModelTextProxy ) {
-		const attributes = modelAttributesToString( item );
-
-		result = attributes ? '<$text' + attributes + '>' + item.data + '</$text>' : item.data;
-	} else {
-		const walker = new ModelWalker( { boundaries: ModelRange.createIn( item ), shallow: true } );
-
-		for ( const value of walker ) {
-			result += modelToString( value.item );
-		}
-
-		if ( item instanceof ModelElement ) {
-			const attributes = modelAttributesToString( item );
-
-			result = '<' + item.name + attributes + '>' + result + '</' + item.name + '>';
-		}
-	}
-
-	return result;
-}
-
-const textAttributes = [ 'linkHref', 'linkTitle', 'bold', 'italic', 'style' ];
-const pAttributes = [ 'class', 'important', 'theme', 'decorated', 'size' ];
-
-describe( 'View converter builder', () => {
-	let dispatcher, model, schema, context;
-
-	beforeEach( () => {
-		model = new Model();
-
-		// `context` parameter for `.convert` calls.
-		context = [ '$root' ];
-
-		schema = model.schema;
-
-		schema.register( 'paragraph', {
-			inheritAllFrom: '$block',
-			allowAttributes: pAttributes
-		} );
-		schema.register( 'div', {
-			inheritAllFrom: '$block',
-			allowAttributes: 'class'
-		} );
-		schema.register( 'customP', {
-			inheritAllFrom: 'paragraph'
-		} );
-		schema.register( 'image', {
-			inheritAllFrom: '$text',
-			allowAttributes: 'src'
-		} );
-		schema.register( 'span', {
-			inheritAllFrom: '$text',
-			allowAttributes: 'transformer'
-		} );
-		// Yes, folks, we are building MEGATRON.
-		schema.register( 'MEGATRON', {
-			inheritAllFrom: '$text'
-		} );
-		schema.register( 'abcd', {
-			inheritAllFrom: '$text'
-		} );
-		schema.extend( '$text', {
-			allowAttributes: textAttributes,
-			allowIn: [ '$root', 'span', 'abcd', 'MEGATRON' ]
-		} );
-
-		dispatcher = new ViewConversionDispatcher( model, { schema } );
-		dispatcher.on( 'text', convertText() );
-	} );
-
-	it( 'should convert from view element to model element', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-
-		const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ) );
-
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
-	} );
-
-	it( 'should convert from view element to model element using creator function', () => {
-		buildViewConverter().for( dispatcher )
-			.fromElement( 'img' )
-			.toElement( viewElement => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) );
-
-		const conversionResult = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), context );
-
-		expect( modelToString( conversionResult ) ).to.equal( '<image src="foo.jpg"></image>' );
-	} );
-
-	it( 'should convert to model element when element children are not allowed in parent (empty split elements should be removed)', () => {
-		schema.register( 'section', {
-			inheritAllFrom: '$block'
-		} );
-
-		buildViewConverter().for( dispatcher )
-			.fromElement( 'p' )
-			.toElement( 'paragraph' );
-
-		buildViewConverter().for( dispatcher )
-			.fromElement( 'section' )
-			.toElement( 'section' );
-
-		const input = new ViewContainerElement( 'p', null, [
-			new ViewText( 'foo' ),
-			new ViewContainerElement( 'section', null, [
-				new ViewContainerElement( 'p', null, [
-					new ViewText( 'abc' ),
-					new ViewContainerElement( 'section' ),
-					new ViewText( 'cde' ),
-				] )
-			] ),
-			new ViewText( 'bar' ),
-		] );
-
-		const conversionResult = dispatcher.convert( input );
-
-		expect( modelToString( conversionResult ) ).to.equal(
-			'<paragraph>foo</paragraph>' +
-			'<paragraph>abc</paragraph>' +
-			'<section></section>' +
-			'<paragraph>cde</paragraph>' +
-			'<paragraph>bar</paragraph>'
-		);
-	} );
-
-	it( 'should convert from view element to model attribute', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'strong' ).toAttribute( 'bold', true );
-
-		const conversionResult = dispatcher.convert(
-			new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), context
-		);
-
-		// Have to check root because result is a ModelText.
-		expect( modelToString( conversionResult ) ).to.equal( '<$text bold="true">foo</$text>' );
-	} );
-
-	it( 'should convert from view element to model attributes using creator function', () => {
-		buildViewConverter().for( dispatcher )
-			.fromElement( 'a' )
-			.toAttribute( viewElement => ( { key: 'linkHref', value: viewElement.getAttribute( 'href' ) } ) );
-
-		const conversionResult = dispatcher.convert(
-			new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), context
-		);
-
-		// Have to check root because result is a ModelText.
-		expect( modelToString( conversionResult ) ).to.equal( '<$text linkHref="foo.html">foo</$text>' );
-	} );
-
-	it( 'should convert from view attribute to model attribute', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-
-		buildViewConverter().for( dispatcher )
-			.fromAttribute( 'class' )
-			.toAttribute( viewElement => ( { key: 'class', value: viewElement.getAttribute( 'class' ) } ) );
-
-		const conversionResult = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), context
-		);
-
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
-	} );
-
-	it( 'should convert from view attribute and key to model attribute', () => {
-		schema.extend( 'paragraph', { allowAttributes: 'type' } );
-
-		dispatcher.on( 'documentFragment', convertToModelFragment() );
-
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-		buildViewConverter().for( dispatcher ).fromAttribute( 'class', 'important' ).toAttribute( 'important', true );
-		buildViewConverter().for( dispatcher ).fromAttribute( 'class', 'theme-nice' ).toAttribute( 'theme', 'nice' );
-		buildViewConverter().for( dispatcher ).fromAttribute( 'data-type' ).toAttribute( 'type' );
-
-		const viewStructure = new ViewDocumentFragment( [
-			new ViewContainerElement( 'p', { class: 'important' }, new ViewText( 'foo' ) ),
-			new ViewContainerElement( 'p', { class: 'important theme-nice' }, new ViewText( 'bar' ) ),
-			new ViewContainerElement( 'p', { 'data-type': 'foo' }, new ViewText( 'xyz' ) )
-		] );
-
-		const conversionResult = dispatcher.convert( viewStructure, context );
-
-		expect( modelToString( conversionResult ) ).to.equal(
-			'<paragraph important="true">foo</paragraph>' +
-			'<paragraph important="true" theme="nice">bar</paragraph>' +
-			'<paragraph type="foo">xyz</paragraph>'
-		);
-	} );
-
-	it( 'should convert from multiple view entities to model attribute', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-
-		buildViewConverter().for( dispatcher )
-			.fromElement( 'strong' )
-			.fromElement( 'b' )
-			.fromAttribute( 'class', 'bold' )
-			.fromAttribute( 'style', { 'font-weight': 'bold' } )
-			.toAttribute( 'bold', true );
-
-		const viewElement = new ViewContainerElement( 'p', null, [
-			new ViewAttributeElement( 'strong', null, new ViewText( 'aaa' ) ),
-			new ViewAttributeElement( 'b', null, new ViewText( 'bbb' ) ),
-			new ViewContainerElement( 'span', { class: 'bold' }, new ViewText( 'ccc' ) ),
-			new ViewContainerElement( 'span', { style: 'font-weight:bold; font-size:20px' }, new ViewText( 'ddd' ) )
-		] );
-
-		const conversionResult = dispatcher.convert( viewElement, context );
-
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph><$text bold="true">aaabbbcccddd</$text></paragraph>' );
-	} );
-
-	it( 'should convert from pattern to marker', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-		buildViewConverter().for( dispatcher ).from( { attribute: { 'data-name': 'search' } } ).toMarker();
-
-		const viewElement = new ViewContainerElement( 'p', null, [
-			new ViewText( 'Fo' ),
-			new ViewAttributeElement( 'marker', { 'data-name': 'search' } ),
-			new ViewText( 'o ba' ),
-			new ViewAttributeElement( 'marker', { 'data-name': 'search' } ),
-			new ViewText( 'r' )
-		] );
-
-		const conversionResult = dispatcher.convert( viewElement, context );
-
-		const markerSearch = conversionResult.markers.get( 'search' );
-
-		expect( conversionResult.markers.size ).to.equal( 1 );
-		expect( markerSearch.start.path ).to.deep.equal( [ 0, 2 ] );
-		expect( markerSearch.end.path ).to.deep.equal( [ 0, 6 ] );
-	} );
-
-	it( 'should convert from element to marker using creator function', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-		buildViewConverter().for( dispatcher ).fromElement( 'marker' ).toMarker( data => {
-			return new ModelElement( '$marker', { 'data-name': data.getAttribute( 'class' ) } );
-		} );
-
-		const viewElement = new ViewContainerElement( 'p', null, [
-			new ViewText( 'Fo' ),
-			new ViewAttributeElement( 'marker', { 'class': 'search' } ),
-			new ViewText( 'o ba' ),
-			new ViewAttributeElement( 'marker', { 'class': 'search' } ),
-			new ViewText( 'r' )
-		] );
-
-		const conversionResult = dispatcher.convert( viewElement, context );
-
-		const markerSearch = conversionResult.markers.get( 'search' );
-
-		expect( conversionResult.markers.size ).to.equal( 1 );
-		expect( markerSearch.start.path ).to.deep.equal( [ 0, 2 ] );
-		expect( markerSearch.end.path ).to.deep.equal( [ 0, 6 ] );
-	} );
-
-	it( 'should convert from multiple view entities to marker', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-		buildViewConverter().for( dispatcher )
-			.from( { attribute: { 'foo': 'marker' } } )
-			.from( { attribute: { 'bar': 'marker' } } )
-			.from( { attribute: { 'foo': 'marker', 'bar': 'marker' } } )
-			.toMarker();
-
-		const viewElement = new ViewContainerElement( 'p', null, [
-			new ViewText( 'Fo' ),
-			new ViewAttributeElement( 'span', { 'foo': 'marker', 'data-name': 'marker1' } ),
-			new ViewText( 'o b' ),
-			new ViewAttributeElement( 'span', { 'bar': 'marker', 'data-name': 'marker2' } ),
-			new ViewText( 'a' ),
-			new ViewAttributeElement( 'span', { 'foo': 'marker', 'bar': 'marker', 'data-name': 'marker3' } ),
-			new ViewText( 'r' )
-		] );
-
-		const conversionResult = dispatcher.convert( viewElement );
-
-		const marker1 = conversionResult.markers.get( 'marker1' );
-		const marker2 = conversionResult.markers.get( 'marker2' );
-		const marker3 = conversionResult.markers.get( 'marker3' );
-
-		expect( conversionResult.markers.size ).to.equal( 3 );
-		expect( marker1.start.path ).to.deep.equal( marker1.end.path ).to.deep.equal( [ 0, 2 ] );
-		expect( marker2.start.path ).to.deep.equal( marker2.end.path ).to.deep.equal( [ 0, 5 ] );
-		expect( marker3.start.path ).to.deep.equal( marker3.end.path ).to.deep.equal( [ 0, 6 ] );
-	} );
-
-	it( 'should do nothing when there is no element matching to marker pattern', () => {
-		buildViewConverter().for( dispatcher ).from( { class: 'color' } ).toMarker();
-
-		const element = new ViewAttributeElement( 'span' );
-
-		const result = dispatcher.convert( element );
-
-		expect( result ).to.be.instanceof( ModelDocumentFragment );
-		expect( result.childCount ).to.equal( 0 );
-	} );
-
-	it( 'should throw an error when view element in not valid to convert to marker', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'marker' ).toMarker();
-
-		const element = new ViewAttributeElement( 'marker', { class: 'search' } );
-
-		expect( () => {
-			dispatcher.convert( element, context );
-		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
-	} );
-
-	it( 'should throw an error when model element returned by creator has not valid name', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'marker' ).toMarker( () => {
-			return new ModelElement( 'element', { 'data-name': 'search' } );
-		} );
-
-		const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } );
-
-		expect( () => {
-			dispatcher.convert( element, context );
-		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
-	} );
-
-	it( 'should throw an error when model element returned by creator has not valid data-name attribute', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'marker' ).toMarker( () => {
-			return new ModelElement( '$marker', { 'foo': 'search' } );
-		} );
-
-		const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } );
-
-		expect( () => {
-			dispatcher.convert( element, context );
-		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
-	} );
-
-	it( 'should convert from pattern to model element', () => {
-		buildViewConverter().for( dispatcher ).from(
-			{ name: 'span', class: 'megatron', attribute: { head: 'megatron', body: 'megatron', legs: 'megatron' } }
-		).toElement( 'MEGATRON' );
-
-		// Adding callbacks later so they are called later. MEGATRON callback is more important.
-		buildViewConverter().for( dispatcher ).fromElement( 'span' ).toElement( 'span' );
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-
-		let result;
-
-		// Not quite megatron.
-		result = dispatcher.convert(
-			new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), context
-		);
-
-		expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
-
-		// Almost a megatron. Missing a head.
-		result = dispatcher.convert(
-			new ViewContainerElement( 'span', { class: 'megatron', body: 'megatron', legs: 'megatron' }, new ViewText( 'foo' ) ),
-			context
-		);
-
-		expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
-
-		// This would be a megatron but is a paragraph.
-		result = dispatcher.convert(
-			new ViewContainerElement(
-				'p',
-				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
-				new ViewText( 'foo' )
-			),
-			context
-		);
-
-		expect( modelToString( result ) ).to.equal( '<paragraph>foo</paragraph>' );
-
-		// At last we have a megatron!
-		result = dispatcher.convert(
-			new ViewContainerElement(
-				'span',
-				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
-				new ViewText( 'foo' )
-			),
-			context
-		);
-
-		expect( modelToString( result ) ).to.equal( '<MEGATRON>foo</MEGATRON>' );
-	} );
-
-	it( 'should convert from pattern to model attribute', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'span' ).toElement( 'span' );
-
-		// This time without name so default span converter will convert children.
-		buildViewConverter().for( dispatcher )
-			.from( { class: 'megatron', attribute: { head: 'megatron', body: 'megatron', legs: 'megatron' } } )
-			.toAttribute( 'transformer', 'megatron' );
-
-		const viewElement = new ViewContainerElement(
-			'span',
-			{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
-			new ViewText( 'foo' )
-		);
-
-		const conversionResult = dispatcher.convert( viewElement, context );
-
-		expect( modelToString( conversionResult ) ).to.equal( '<span transformer="megatron">foo</span>' );
-	} );
-
-	it( 'should return model document fragment when converting attributes on text', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'strong' ).toAttribute( 'bold', true );
-
-		const viewElement = new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) );
-
-		const conversionResult = dispatcher.convert( viewElement, context );
-
-		expect( conversionResult.is( 'documentFragment' ) ).to.be.true;
-	} );
-
-	it( 'should set different priorities for `toElement` and `toAttribute` conversion', () => {
-		buildViewConverter().for( dispatcher )
-			.fromAttribute( 'class' )
-			.toAttribute( viewElement => ( { key: 'class', value: viewElement.getAttribute( 'class' ) } ) );
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-
-		const conversionResult = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), context
-		);
-
-		// Element converter was fired first even though attribute converter was added first.
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
-	} );
-
-	it( 'should overwrite default priorities for converters', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-		buildViewConverter().for( dispatcher )
-			.fromAttribute( 'class' )
-			.toAttribute( viewElement => ( { key: 'class', value: viewElement.getAttribute( 'class' ) } ) );
-
-		let result;
-
-		result = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), context
-		);
-
-		expect( modelToString( result ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
-
-		buildViewConverter().for( dispatcher )
-			.from( { name: 'p', class: 'myClass' } ).withPriority( 'high' )
-			.toElement( 'customP' );
-
-		result = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), context
-		);
-
-		expect( modelToString( result ) ).to.equal( '<customP>foo</customP>' );
-	} );
-
-	it( 'should overwrite default consumed values', () => {
-		// Converter (1).
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-
-		// Converter (2).
-		buildViewConverter().for( dispatcher )
-			.from( { name: 'p', class: 'decorated' } ).consuming( { class: 'decorated' } )
-			.toAttribute( 'decorated', true );
-
-		// Converter (3).
-		buildViewConverter().for( dispatcher )
-			.fromAttribute( 'class', 'small' ).consuming( { class: 'small' } )
-			.toAttribute( 'size', 'small' );
-
-		const viewElement = new ViewContainerElement( 'p', { class: 'decorated small' }, new ViewText( 'foo' ) );
-		const conversionResult = dispatcher.convert( viewElement, context );
-
-		// P element and it's children got converted by the converter (1) and the converter (1) got fired
-		// because P name was not consumed in converter (2). Converter (3) could consume class="small" because
-		// only class="decorated" was consumed in converter (2).
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph decorated="true" size="small">foo</paragraph>' );
-	} );
-
-	it( 'should convert from matcher instance to model', () => {
-		// Universal class converter, synonymous to .fromAttribute( 'class' ).
-		buildViewConverter().for( dispatcher )
-			.from( new ViewMatcher( { class: /.*/ } ) )
-			.toAttribute( viewElement => ( { key: 'class', value: viewElement.getAttribute( 'class' ) } ) );
-
-		// Universal element converter.
-		buildViewConverter().for( dispatcher )
-			.from( new ViewMatcher( { name: /.*/ } ) )
-			.toElement( viewElement => new ModelElement( viewElement.name ) );
-
-		const viewStructure = new ViewContainerElement( 'div', { class: 'myClass' }, [
-			new ViewContainerElement( 'abcd', null, new ViewText( 'foo' ) )
-		] );
-
-		const conversionResult = dispatcher.convert( viewStructure, context );
-
-		expect( modelToString( conversionResult ) ).to.equal( '<div class="myClass"><abcd>foo</abcd></div>' );
-	} );
-
-	it( 'should filter out structure that is wrong with schema - elements', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'div' ).toElement( 'div' );
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-
-		// Disallow $root>div.
-		schema.addChildCheck( ( ctx, childDef ) => {
-			if ( childDef.name == 'div' && ctx.endsWith( '$root' ) ) {
-				return false;
-			}
-		} );
-
-		dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
-
-		const viewElement = new ViewContainerElement( 'div', null,
-			new ViewContainerElement( 'p', null,
-				new ViewText( 'foo' )
-			)
-		);
-
-		const conversionResult = dispatcher.convert( viewElement, context );
-
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
-	} );
-
-	it( 'should filter out structure that is wrong with schema - attributes', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-		buildViewConverter().for( dispatcher ).fromElement( 'strong' ).toAttribute( 'bold', true );
-
-		// Disallow bold in paragraph>$text.
-		schema.addAttributeCheck( ( ctx, attributeName ) => {
-			if ( ctx.endsWith( 'paragraph $text' ) && attributeName == 'bold' ) {
-				return false;
-			}
-		} );
-
-		dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
-
-		const viewElement = new ViewContainerElement( 'p', null,
-			new ViewAttributeElement( 'strong', null,
-				new ViewText( 'foo' )
-			)
-		);
-
-		const conversionResult = dispatcher.convert( viewElement, context );
-
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
-	} );
-
-	it( 'should not set attribute when it is not allowed', () => {
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-		buildViewConverter().for( dispatcher ).fromElement( 'u' ).toAttribute( 'underscore', true );
-
-		const viewElement = new ViewContainerElement( 'p', null,
-			new ViewAttributeElement( 'u', null,
-				new ViewText( 'foo' )
-			)
-		);
-
-		const conversionResult = dispatcher.convert( viewElement, context );
-
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
-	} );
-
-	it( 'should stop to element conversion if creating function returned null', () => {
-		buildViewConverter()
-			.for( dispatcher )
-			.fromElement( 'p' )
-			.toElement( viewElement => {
-				return viewElement.hasAttribute( 'stop' ) ? null : new ModelElement( 'paragraph' );
-			} );
-
-		const viewElement = new ViewContainerElement( 'p' );
-		let conversionResult = dispatcher.convert( viewElement, context );
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph></paragraph>' );
-
-		viewElement.setAttribute( 'stop', true );
-		conversionResult = dispatcher.convert( viewElement, context );
-
-		expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
-		expect( conversionResult.childCount ).to.equal( 0 );
-	} );
-
-	it( 'should stop to attribute conversion if creating function returned null', () => {
-		schema.extend( 'paragraph', { allowAttributes: 'type' } );
-
-		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-
-		buildViewConverter().for( dispatcher ).fromAttribute( 'data-type' ).toAttribute( viewElement => {
-			const value = viewElement.getAttribute( 'data-type' );
-
-			return value == 'stop' ? null : { key: 'type', value };
-		} );
-
-		const viewElement = new ViewContainerElement( 'p', { 'data-type': 'foo' } );
-		let conversionResult = dispatcher.convert( viewElement, context );
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph type="foo"></paragraph>' );
-
-		viewElement.setAttribute( 'data-type', 'stop' );
-		conversionResult = dispatcher.convert( viewElement, context );
-		expect( modelToString( conversionResult ) ).to.equal( '<paragraph></paragraph>' );
-	} );
-} );

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

@@ -0,0 +1,73 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Conversion from '../../src/conversion/conversion';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+describe( 'Conversion', () => {
+	let conversion, dispA, dispB;
+
+	beforeEach( () => {
+		conversion = new Conversion();
+
+		// Placeholders. Will be used only to see if their were given as attribute for a spy function.
+		dispA = Symbol( 'dispA' );
+		dispB = Symbol( 'dispB' );
+
+		conversion.register( 'ab', [ dispA, dispB ] );
+		conversion.register( 'a', [ dispA ] );
+		conversion.register( 'b', [ dispB ] );
+	} );
+
+	describe( 'register()', () => {
+		it( 'should throw when trying to use same group name twice', () => {
+			expect( () => {
+				conversion.register( 'ab' );
+			} ).to.throw( CKEditorError, /conversion-register-group-exists/ );
+		} );
+	} );
+
+	describe( 'for()', () => {
+		it( 'should return object with .add() method', () => {
+			const forResult = conversion.for( 'ab' );
+
+			expect( forResult.add ).to.be.instanceof( Function );
+		} );
+
+		it( 'should throw if non-existing group name has been used', () => {
+			expect( () => {
+				conversion.for( 'foo' );
+			} ).to.throw( CKEditorError, /conversion-for-unknown-group/ );
+		} );
+	} );
+
+	describe( 'add()', () => {
+		let helperA, helperB;
+
+		beforeEach( () => {
+			helperA = sinon.stub();
+			helperB = sinon.stub();
+		} );
+
+		it( 'should be chainable', () => {
+			const forResult = conversion.for( 'ab' );
+			const addResult = forResult.add( () => {} );
+
+			expect( addResult ).to.equal( addResult.add( () => {} ) );
+		} );
+
+		it( 'should fire given helper for every dispatcher in given group', () => {
+			conversion.for( 'ab' ).add( helperA );
+
+			expect( helperA.calledWithExactly( dispA ) ).to.be.true;
+			expect( helperA.calledWithExactly( dispB ) ).to.be.true;
+
+			conversion.for( 'b' ).add( helperB );
+
+			expect( helperB.calledWithExactly( dispA ) ).to.be.false;
+			expect( helperB.calledWithExactly( dispB ) ).to.be.true;
+		} );
+	} );
+} );

+ 0 - 484
packages/ckeditor5-engine/tests/conversion/definition-based-converters.js

@@ -1,484 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import ModelElement from '../../src/model/element';
-import ModelText from '../../src/model/text';
-import ModelRange from '../../src/model/range';
-
-import ViewElement from '../../src/view/element';
-import ViewAttributeElement from '../../src/view/attributeelement';
-import ViewText from '../../src/view/text';
-
-import { convertText } from '../../src/conversion/view-to-model-converters';
-
-import {
-	modelAttributeToViewAttributeElement,
-	viewToModelAttribute,
-	modelElementToViewContainerElement,
-	viewToModelElement
-} from '../../src/conversion/definition-based-converters';
-
-import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
-import ModelWalker from '../../src/model/treewalker';
-import ModelTextProxy from '../../src/model/textproxy';
-import Model from '../../src/model/model';
-import ModelPosition from '../../src/model/position';
-import EditingController from '../../src/controller/editingcontroller';
-
-function viewAttributesToString( item ) {
-	let result = '';
-
-	for ( const key of item.getAttributeKeys() ) {
-		const value = item.getAttribute( key );
-
-		if ( value ) {
-			result += ' ' + key + '="' + value + '"';
-		}
-	}
-
-	return result;
-}
-
-function modelToString( item ) {
-	let result = '';
-
-	if ( item instanceof ModelTextProxy ) {
-		const attributes = modelAttributesToString( item );
-
-		result = attributes ? '<$text' + attributes + '>' + item.data + '</$text>' : item.data;
-	} else {
-		const walker = new ModelWalker( { boundaries: ModelRange.createIn( item ), shallow: true } );
-
-		for ( const value of walker ) {
-			result += modelToString( value.item );
-		}
-
-		if ( item instanceof ModelElement ) {
-			const attributes = modelAttributesToString( item );
-
-			result = '<' + item.name + attributes + '>' + result + '</' + item.name + '>';
-		}
-	}
-
-	return result;
-}
-
-function modelAttributesToString( item ) {
-	let result = '';
-
-	for ( const attr of item.getAttributes() ) {
-		result += ' ' + attr[ 0 ] + '="' + attr[ 1 ] + '"';
-	}
-
-	return result;
-}
-
-function viewToString( item ) {
-	let result = '';
-
-	if ( item instanceof ViewText ) {
-		result = item.data;
-	} else {
-		// ViewElement or ViewDocumentFragment.
-		for ( const child of item.getChildren() ) {
-			result += viewToString( child );
-		}
-
-		if ( item instanceof ViewElement ) {
-			result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
-		}
-	}
-
-	return result;
-}
-
-describe( 'definition-based-converters', () => {
-	let model, dispatcher, modelDoc, modelRoot, viewRoot, controller, context, schema;
-
-	beforeEach( () => {
-		model = new Model();
-	} );
-
-	function setupViewToModelTests() {
-		context = [ '$root' ];
-		schema = model.schema;
-		dispatcher = new ViewConversionDispatcher( model, { schema } );
-	}
-
-	function setupModelToViewTests() {
-		modelDoc = model.document;
-		modelRoot = modelDoc.createRoot();
-
-		controller = new EditingController( model );
-
-		// Set name of view root the same as dom root.
-		// This is a mock of attaching view root to dom root.
-		controller.view.getRoot()._name = 'div';
-
-		viewRoot = controller.view.getRoot();
-		dispatcher = controller.modelToView;
-	}
-
-	describe( 'Attribute converters', () => {
-		function testModelConversion( definition, expectedConversion ) {
-			modelAttributeToViewAttributeElement( 'foo', [ definition ], [ dispatcher ] );
-
-			const modelElement = new ModelText( 'foo', { foo: 'bar' } );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( expectedConversion );
-
-			model.change( writer => {
-				writer.removeAttribute( 'foo', modelElement );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
-		}
-
-		describe( 'model to view conversion', () => {
-			beforeEach( () => {
-				setupModelToViewTests();
-			} );
-
-			it( 'using passed view element name', () => {
-				testModelConversion( { model: 'bar', view: 'strong' }, '<div><strong>foo</strong></div>' );
-			} );
-
-			it( 'using passed view element object', () => {
-				testModelConversion( { model: 'bar', view: { name: 'strong' } }, '<div><strong>foo</strong></div>' );
-			} );
-
-			it( 'using passed view element object with style object', () => {
-				testModelConversion( {
-					model: 'bar',
-					view: { name: 'span', style: { 'font-weight': 'bold' } }
-				}, '<div><span style="font-weight:bold;">foo</span></div>' );
-			} );
-
-			it( 'using passed view element object with class string', () => {
-				testModelConversion( { model: 'bar', view: { name: 'span', class: 'foo' } }, '<div><span class="foo">foo</span></div>' );
-			} );
-
-			it( 'using passed view element object with class array', () => {
-				testModelConversion( {
-					model: 'bar',
-					view: { name: 'span', class: [ 'foo', 'foo-bar' ] }
-				}, '<div><span class="foo foo-bar">foo</span></div>' );
-			} );
-
-			it( 'using passed view element object with attributes', () => {
-				testModelConversion( {
-					model: 'bar',
-					view: { name: 'span', attribute: { 'data-foo': 'bar' } }
-				}, '<div><span data-foo="bar">foo</span></div>' );
-			} );
-
-			it( 'should convert when changing attribute', () => {
-				const definition1 = { model: 'bar', view: { name: 'span', class: 'bar' } };
-				const definition2 = { model: 'baz', view: { name: 'span', class: 'baz' } };
-
-				modelAttributeToViewAttributeElement( 'foo', [ definition1, definition2 ], [ dispatcher ] );
-
-				const modelElement = new ModelText( 'foo', { foo: 'bar' } );
-
-				model.change( writer => {
-					writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><span class="bar">foo</span></div>' );
-
-				model.change( writer => {
-					writer.setAttribute( 'foo', 'baz', modelElement );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><span class="baz">foo</span></div>' );
-			} );
-
-			it( 'should do nothing for undefined value', () => {
-				modelAttributeToViewAttributeElement( 'foo', [ { model: 'bar', view: 'strong' } ], [ dispatcher ] );
-
-				const modelElement = new ModelText( 'foo', { foo: 'baz' } );
-
-				model.change( writer => {
-					writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
-			} );
-		} );
-
-		describe( 'view to model conversion', () => {
-			beforeEach( () => {
-				setupViewToModelTests();
-
-				schema.register( 'div', { inheritAllFrom: '$block' } );
-				schema.extend( '$text', {
-					allowIn: '$root',
-					allowAttributes: 'foo'
-				} );
-
-				dispatcher.on( 'text', convertText() );
-			} );
-
-			it( 'should convert using element name', () => {
-				viewToModelAttribute( 'foo', { model: 'bar', view: 'strong' }, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
-			} );
-
-			it( 'should convert using object', () => {
-				viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'strong' } }, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
-			} );
-
-			it( 'should convert using class string', () => {
-				viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'span', class: 'foo' } }, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
-			} );
-
-			it( 'should convert using class array', () => {
-				viewToModelAttribute( 'foo', {
-					model: 'bar',
-					view: { name: 'span', class: [ 'foo', 'bar' ] }
-				}, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
-			} );
-
-			it( 'should convert using style object', () => {
-				viewToModelAttribute( 'foo', {
-					model: 'bar',
-					view: { name: 'span', style: { 'font-weight': 'bold' } }
-				}, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
-			} );
-
-			it( 'should convert using attributes object', () => {
-				viewToModelAttribute( 'foo', {
-					model: 'bar',
-					view: { name: 'span', attribute: { 'data-foo': 'bar' } }
-				}, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
-			} );
-
-			it( 'should convert using acceptAlso array', () => {
-				viewToModelAttribute( 'foo', {
-					model: 'bar',
-					view: 'strong',
-					acceptsAlso: [
-						{ name: 'span', class: [ 'foo', 'bar' ] },
-						{ name: 'span', attribute: { 'data-foo': 'bar' } }
-					]
-				}, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
-			} );
-
-			it( 'should convert using priority', () => {
-				viewToModelAttribute( 'foo', { model: 'baz', view: 'strong' }, [ dispatcher ] );
-				viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'strong', priority: 'high' } }, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
-			} );
-		} );
-	} );
-
-	describe( 'Element converters', () => {
-		function testModelConversion( definition, expectedResult ) {
-			modelElementToViewContainerElement( definition, [ dispatcher ] );
-
-			const modelElement = new ModelElement( 'foo', null, new ModelText( 'bar' ) );
-
-			model.change( writer => {
-				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div>' + expectedResult + '</div>' );
-		}
-
-		describe( 'model to view conversion', () => {
-			beforeEach( () => {
-				setupModelToViewTests();
-			} );
-
-			it( 'using passed view element name', () => {
-				testModelConversion( { model: 'foo', view: 'code' }, '<code>bar</code>' );
-			} );
-
-			it( 'using passed view element object', () => {
-				testModelConversion( { model: 'foo', view: { name: 'code' } }, '<code>bar</code>' );
-			} );
-
-			it( 'using passed view element object with style object', () => {
-				testModelConversion( {
-					model: 'foo',
-					view: { name: 'span', style: { 'font-weight': 'bold' } }
-				}, '<span style="font-weight:bold;">bar</span>' );
-			} );
-
-			it( 'using passed view element object with class string', () => {
-				testModelConversion( { model: 'foo', view: { name: 'span', class: 'foo' } }, '<span class="foo">bar</span>' );
-			} );
-
-			it( 'using passed view element object with class array', () => {
-				testModelConversion( {
-					model: 'foo',
-					view: { name: 'span', class: [ 'foo', 'foo-bar' ] }
-				}, '<span class="foo foo-bar">bar</span>' );
-			} );
-
-			it( 'using passed view element object with attributes', () => {
-				testModelConversion( {
-					model: 'foo',
-					view: { name: 'span', attribute: { 'data-foo': 'bar' } }
-				}, '<span data-foo="bar">bar</span>' );
-			} );
-		} );
-
-		describe( 'view to model conversion', () => {
-			beforeEach( () => {
-				setupViewToModelTests();
-
-				schema.register( 'div', { inheritAllFrom: '$block' } );
-				schema.register( 'bar', { inheritAllFrom: '$block' } );
-				schema.register( 'baz', { inheritAllFrom: '$block' } );
-
-				schema.extend( '$text', {
-					allowIn: '$root',
-					allowAttributes: 'foo'
-				} );
-
-				dispatcher.on( 'text', convertText() );
-			} );
-
-			it( 'should convert using element name', () => {
-				viewToModelElement( { model: 'bar', view: 'strong' }, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
-			} );
-
-			it( 'should convert using object', () => {
-				viewToModelElement( { model: 'bar', view: { name: 'strong' } }, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
-			} );
-
-			it( 'should convert using class string', () => {
-				viewToModelElement( { model: 'bar', view: { name: 'span', class: 'foo' } }, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
-			} );
-
-			it( 'should convert using class array', () => {
-				viewToModelElement( { model: 'bar', view: { name: 'span', class: [ 'foo', 'bar' ] } }, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
-			} );
-
-			it( 'should convert using style object', () => {
-				viewToModelElement( { model: 'bar', view: { name: 'span', style: { 'font-weight': 'bold' } } }, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
-			} );
-
-			it( 'should convert using attributes object', () => {
-				viewToModelElement( { model: 'bar', view: { name: 'span', attribute: { 'data-foo': 'bar' } } }, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
-			} );
-
-			it( 'should convert using acceptAlso array', () => {
-				viewToModelElement( {
-					model: 'bar',
-					view: 'strong',
-					acceptsAlso: [
-						{ name: 'span', class: [ 'foo', 'bar' ] },
-						{ name: 'span', attribute: { 'data-foo': 'bar' } }
-					]
-				}, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
-			} );
-
-			it( 'should convert using priority', () => {
-				viewToModelElement( { model: 'baz', view: 'strong' }, [ dispatcher ] );
-				viewToModelElement( { model: 'bar', view: { name: 'strong', priority: 'high' } }, [ dispatcher ] );
-
-				const conversionResult = dispatcher.convert(
-					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), context
-				);
-
-				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
-			} );
-		} );
-	} );
-} );

+ 547 - 21
packages/ckeditor5-engine/tests/conversion/model-to-view-converters.js

@@ -3,6 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
+import EditingController from '../../src/controller/editingcontroller';
+
+import Conversion from '../../src/conversion/conversion';
+
 import Model from '../../src/model/model';
 import ModelElement from '../../src/model/element';
 import ModelText from '../../src/model/text';
@@ -16,20 +20,520 @@ import ViewUIElement from '../../src/view/uielement';
 import ViewText from '../../src/view/text';
 
 import {
-	insertElement,
-	insertUIElement,
-	changeAttribute,
-	wrap,
-	removeUIElement,
-	highlightElement,
-	highlightText,
-	removeHighlight,
-	createViewElementFromHighlightDescriptor
-} from '../../src/conversion/model-to-view-converters';
+	downcastElementToElement, downcastAttributeToElement, downcastAttributeToAttribute, downcastMarkerToElement, downcastMarkerToHighlight,
+	insertElement, insertUIElement, changeAttribute, wrap, removeUIElement,
+	highlightElement, highlightText, removeHighlight, createViewElementFromHighlightDescriptor
+} from '../../src/conversion/downcast-converters';
 
-import EditingController from '../../src/controller/editingcontroller';
+import { stringify } from '../../src/dev-utils/view';
+
+describe( 'downcast-helpers', () => {
+	let conversion, model, modelRoot, viewRoot;
+
+	beforeEach( () => {
+		model = new Model();
+		const modelDoc = model.document;
+		modelRoot = modelDoc.createRoot();
+
+		const controller = new EditingController( model );
+
+		// Set name of view root the same as dom root.
+		// This is a mock of attaching view root to dom root.
+		controller.view.getRoot()._name = 'div';
+
+		viewRoot = controller.view.getRoot();
+
+		conversion = new Conversion();
+		conversion.register( 'downcast', [ controller.downcastDispatcher ] );
+	} );
+
+	describe( 'downcastElementToElement', () => {
+		it( 'config.view is a string', () => {
+			const helper = downcastElementToElement( { model: 'paragraph', view: 'p' } );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'paragraph', modelRoot, 0 );
+			} );
+
+			expectResult( '<p></p>' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = downcastElementToElement( { model: 'paragraph', view: 'p' } );
+			const helperB = downcastElementToElement( { model: 'paragraph', view: 'foo' }, 'high' );
+
+			conversion.for( 'downcast' ).add( helperA ).add( helperB );
+
+			model.change( writer => {
+				writer.insertElement( 'paragraph', modelRoot, 0 );
+			} );
+
+			expectResult( '<foo></foo>' );
+		} );
+
+		it( 'config.view is an element instance', () => {
+			const helper = downcastElementToElement( {
+				model: 'paragraph',
+				view: new ViewContainerElement( 'p' )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'paragraph', modelRoot, 0 );
+			} );
+
+			expectResult( '<p></p>' );
+		} );
+
+		it( 'config.view is a view element definition', () => {
+			const helper = downcastElementToElement( {
+				model: 'fancyParagraph',
+				view: {
+					name: 'p',
+					class: 'fancy'
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'fancyParagraph', modelRoot, 0 );
+			} );
+
+			expectResult( '<p class="fancy"></p>' );
+		} );
+
+		it( 'config.view is a function', () => {
+			const helper = downcastElementToElement( {
+				model: 'heading',
+				view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'heading', { level: 2 }, modelRoot, 0 );
+			} );
+
+			expectResult( '<h2></h2>' );
+		} );
+	} );
+
+	describe( 'downcastAttributeToElement', () => {
+		it( 'config.view is a string', () => {
+			const helper = downcastAttributeToElement( 'bold', { view: 'strong' } );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
+			} );
+
+			expectResult( '<strong>foo</strong>' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = downcastAttributeToElement( 'bold', { view: 'strong' } );
+			const helperB = downcastAttributeToElement( 'bold', { view: 'b' }, 'high' );
+
+			conversion.for( 'downcast' ).add( helperA ).add( helperB );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
+			} );
+
+			expectResult( '<b>foo</b>' );
+		} );
+
+		it( 'config.view is an element instance', () => {
+			const helper = downcastAttributeToElement( 'bold', {
+				view: new ViewAttributeElement( 'strong' )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
+			} );
+
+			expectResult( '<strong>foo</strong>' );
+		} );
+
+		it( 'config.view is a view element definition', () => {
+			const helper = downcastAttributeToElement( 'bold', {
+				view: {
+					name: 'span',
+					class: 'bold'
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
+			} );
+
+			expectResult( '<span class="bold">foo</span>' );
+		} );
+
+		it( 'config.view is a view element definition, model attribute value specified', () => {
+			const helper = downcastAttributeToElement( 'styled', {
+				model: 'dark',
+				view: {
+					name: 'span',
+					class: [ 'styled', 'styled-dark' ]
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { styled: 'dark' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<span class="styled styled-dark">foo</span>' );
+
+			model.change( writer => {
+				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( 'foo' );
+		} );
+
+		it( 'multiple config items', () => {
+			const helper = downcastAttributeToElement( 'fontSize', [
+				{
+					model: 'big',
+					view: {
+						name: 'span',
+						style: {
+							'font-size': '1.2em'
+						}
+					}
+				},
+				{
+					model: 'small',
+					view: {
+						name: 'span',
+						style: {
+							'font-size': '0.8em'
+						}
+					}
+				}
+			] );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { fontSize: 'big' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<span style="font-size:1.2em">foo</span>' );
+
+			model.change( writer => {
+				writer.setAttribute( 'fontSize', 'small', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( '<span style="font-size:0.8em">foo</span>' );
+
+			model.change( writer => {
+				writer.removeAttribute( 'fontSize', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( 'foo' );
+		} );
+
+		it( 'config.view is a function', () => {
+			const helper = downcastAttributeToElement( 'bold', {
+				view: attributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + attributeValue } )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { bold: '500' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<span style="font-weight:500">foo</span>' );
+		} );
+	} );
+
+	describe( 'downcastAttributeToAttribute', () => {
+		beforeEach( () => {
+			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'image', view: 'img' } ) );
+		} );
+
+		it( 'config not set', () => {
+			const helper = downcastAttributeToAttribute( 'src' );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { src: 'foo.jpg' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img src="foo.jpg"></img>' );
+		} );
+
+		it( 'config.view is a string', () => {
+			const helper = downcastAttributeToAttribute( 'source', { view: 'src' } );
+
+			conversion.for( 'downcast' ).add( helper );
 
-describe( 'model-to-view-converters', () => {
+			model.change( writer => {
+				writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img src="foo.jpg"></img>' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = downcastAttributeToAttribute( 'source', { view: 'href' } );
+			const helperB = downcastAttributeToAttribute( 'source', { view: 'src' }, 'high' );
+
+			conversion.for( 'downcast' ).add( helperA ).add( helperB );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img src="foo.jpg"></img>' );
+		} );
+
+		it( 'config.view is an object', () => {
+			const helper = downcastAttributeToAttribute( 'stylish', { view: { key: 'class', value: 'styled' } } );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { stylish: true }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img class="styled"></img>' );
+		} );
+
+		it( 'config.view is an object, model attribute value specified', () => {
+			const helper = downcastAttributeToAttribute( 'styled', {
+				model: 'dark',
+				view: {
+					key: 'class',
+					value: 'styled-dark styled'
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img class="styled styled-dark"></img>' );
+
+			model.change( writer => {
+				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( '<img></img>' );
+		} );
+
+		it( 'multiple config items', () => {
+			const helper = downcastAttributeToAttribute( 'styled', [
+				{
+					model: 'dark',
+					view: {
+						key: 'class',
+						value: 'styled-dark'
+					}
+				},
+				{
+					model: 'light',
+					view: {
+						key: 'class',
+						value: 'styled-light'
+					}
+				}
+			] );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img class="styled-dark"></img>' );
+
+			model.change( writer => {
+				writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( '<img class="styled-light"></img>' );
+
+			model.change( writer => {
+				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( '<img></img>' );
+		} );
+
+		it( 'config.view is a function', () => {
+			const helper = downcastAttributeToAttribute( 'styled', {
+				view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertElement( 'image', { styled: 'pull-out' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img class="styled-pull-out"></img>' );
+		} );
+	} );
+
+	describe( 'downcastMarkerToElement', () => {
+		it( 'config.view is a string', () => {
+			const helper = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+			} );
+
+			expectResult( 'f<marker-search></marker-search>o<marker-search></marker-search>o' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
+			const helperB = downcastMarkerToElement( { model: 'search', view: 'search' }, 'high' );
+
+			conversion.for( 'downcast' ).add( helperA ).add( helperB );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+			} );
+
+			expectResult( 'f<search></search>o<search></search>o' );
+		} );
+
+		it( 'config.view is an element instance', () => {
+			const helper = downcastMarkerToElement( {
+				model: 'search',
+				view: new ViewUIElement( 'span', { 'data-marker': 'search' } )
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+			} );
+
+			expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
+		} );
+
+		it( 'config.view is a view element definition', () => {
+			const helper = downcastMarkerToElement( {
+				model: 'search',
+				view: {
+					name: 'span',
+					attribute: {
+						'data-marker': 'search'
+					}
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+			} );
+
+			expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
+		} );
+
+		it( 'config.view is a function', () => {
+			const helper = downcastMarkerToElement( {
+				model: 'search',
+				view: data => {
+					return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+			} );
+
+			expectResult( 'f<span data-marker="search" data-start="true"></span>o<span data-marker="search" data-start="false"></span>o' );
+		} );
+	} );
+
+	describe( 'downcastMarkerToHighlight', () => {
+		it( 'config.view is a highlight descriptor', () => {
+			const helper = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'comment', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
+			} );
+
+			expectResult( '<span class="comment">foo</span>' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
+			const helperB = downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
+
+			conversion.for( 'downcast' ).add( helperA ).add( helperB );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'comment', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
+			} );
+
+			expectResult( '<span class="new-comment">foo</span>' );
+		} );
+
+		it( 'config.view is a function', () => {
+			const helper = downcastMarkerToHighlight( {
+				model: 'comment',
+				view: data => {
+					const commentType = data.markerName.split( ':' )[ 1 ];
+
+					return {
+						class: [ 'comment', 'comment-' + commentType ]
+					};
+				}
+			} );
+
+			conversion.for( 'downcast' ).add( helper );
+
+			model.change( writer => {
+				writer.insertText( 'foo', modelRoot, 0 );
+				writer.setMarker( 'comment:abc', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
+			} );
+
+			expectResult( '<span class="comment comment-abc">foo</span>' );
+		} );
+	} );
+
+	function expectResult( string ) {
+		expect( stringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string );
+	}
+} );
+
+describe( 'downcast-converters', () => {
 	let dispatcher, modelDoc, modelRoot, viewRoot, controller, modelRootStart, model;
 
 	beforeEach( () => {
@@ -44,7 +548,7 @@ describe( 'model-to-view-converters', () => {
 		// This is a mock of attaching view root to dom root.
 		controller.view.getRoot()._name = 'div';
 
-		dispatcher = controller.modelToView;
+		dispatcher = controller.downcastDispatcher;
 
 		dispatcher.on( 'insert:paragraph', insertElement( () => new ViewContainerElement( 'p' ) ) );
 		dispatcher.on( 'attribute:class', changeAttribute() );
@@ -86,7 +590,7 @@ describe( 'model-to-view-converters', () => {
 	}
 
 	describe( 'insertText', () => {
-		it( 'should convert text insertion in model to view text', () => {
+		it( 'should downcast text', () => {
 			model.change( writer => {
 				writer.insert( new ModelText( 'foobar' ), modelRootStart );
 			} );
@@ -127,8 +631,8 @@ describe( 'model-to-view-converters', () => {
 		} );
 
 		it( 'should take view element function generator as a parameter', () => {
-			const elementGenerator = ( data, consumable ) => {
-				if ( consumable.consume( data.item, 'attribute:nice' ) ) {
+			const elementGenerator = ( modelItem, consumable ) => {
+				if ( consumable.consume( modelItem, 'attribute:nice' ) ) {
 					return new ViewContainerElement( 'div' );
 				}
 
@@ -173,7 +677,7 @@ describe( 'model-to-view-converters', () => {
 		} );
 
 		it( 'should convert insert/change/remove with attribute generating function as a parameter', () => {
-			const themeConverter = ( value, key, data ) => {
+			const themeConverter = ( value, data ) => {
 				if ( data.item instanceof ModelElement && data.item.childCount > 0 ) {
 					value += ' fix-content';
 				}
@@ -798,6 +1302,28 @@ describe( 'model-to-view-converters', () => {
 
 				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
 			} );
+
+			it( 'should do nothing if collapsed marker is converted', () => {
+				const descriptor = { class: 'foo' };
+
+				dispatcher.on( 'addMarker:marker', highlightText( descriptor ), { priority: 'high' } );
+				dispatcher.on( 'addMarker:marker', highlightElement( descriptor ), { priority: 'high' } );
+				dispatcher.on( 'removeMarker:marker', removeHighlight( descriptor ), { priority: 'high' } );
+
+				markerRange = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 0 );
+
+				model.change( () => {
+					model.markers._set( 'marker', markerRange );
+				} );
+
+				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
+
+				model.change( () => {
+					model.markers._remove( 'marker' );
+				} );
+
+				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
+			} );
 		} );
 
 		describe( 'on element', () => {
@@ -930,7 +1456,7 @@ describe( 'model-to-view-converters', () => {
 		it( 'should return attribute element from descriptor object', () => {
 			const descriptor = {
 				class: 'foo-class',
-				attributes: { one: 1, two: 2 },
+				attributes: { one: '1', two: '2' },
 				priority: 7,
 			};
 			const element = createViewElementFromHighlightDescriptor( descriptor );
@@ -948,7 +1474,7 @@ describe( 'model-to-view-converters', () => {
 		it( 'should return attribute element from descriptor object - array with classes', () => {
 			const descriptor = {
 				class: [ 'foo-class', 'bar-class' ],
-				attributes: { one: 1, two: 2 },
+				attributes: { one: '1', two: '2' },
 				priority: 7,
 			};
 			const element = createViewElementFromHighlightDescriptor( descriptor );
@@ -966,7 +1492,7 @@ describe( 'model-to-view-converters', () => {
 
 		it( 'should create element without class', () => {
 			const descriptor = {
-				attributes: { one: 1, two: 2 },
+				attributes: { one: '1', two: '2' },
 				priority: 7,
 			};
 			const element = createViewElementFromHighlightDescriptor( descriptor );
@@ -983,7 +1509,7 @@ describe( 'model-to-view-converters', () => {
 		it( 'should create element without priority', () => {
 			const descriptor = {
 				class: 'foo-class',
-				attributes: { one: 1, two: 2 },
+				attributes: { one: '1', two: '2' },
 			};
 			const element = createViewElementFromHighlightDescriptor( descriptor );
 

+ 6 - 6
packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js

@@ -15,13 +15,13 @@ import ViewUIElement from '../../src/view/uielement';
 import { mergeAttributes } from '../../src/view/writer';
 
 import Mapper from '../../src/conversion/mapper';
-import ModelConversionDispatcher from '../../src/conversion/modelconversiondispatcher';
+import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
 import {
 	convertRangeSelection,
 	convertCollapsedSelection,
 	clearAttributes,
 	clearFakeSelection
-} from '../../src/conversion/model-selection-to-view-converters';
+} from '../../src/conversion/downcast-selection-converters';
 
 import {
 	insertElement,
@@ -30,13 +30,13 @@ import {
 	highlightElement,
 	highlightText,
 	removeHighlight
-} from '../../src/conversion/model-to-view-converters';
+} from '../../src/conversion/downcast-converters';
 
 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( 'model-selection-to-view-converters', () => {
+describe( 'downcast-selection-converters', () => {
 	let dispatcher, mapper, model, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor;
 
 	beforeEach( () => {
@@ -56,7 +56,7 @@ describe( 'model-selection-to-view-converters', () => {
 
 		highlightDescriptor = { class: 'marker', priority: 1 };
 
-		dispatcher = new ModelConversionDispatcher( model, { mapper, viewSelection } );
+		dispatcher = new DowncastDispatcher( model, { mapper, viewSelection } );
 
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'attribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
@@ -492,7 +492,7 @@ describe( 'model-selection-to-view-converters', () => {
 			model.schema.extend( '$text', { allowIn: 'td' } );
 
 			// "Universal" converter to convert table structure.
-			const tableConverter = insertElement( data => new ViewContainerElement( data.item.name ) );
+			const tableConverter = insertElement( modelItem => new ViewContainerElement( modelItem.name ) );
 			dispatcher.on( 'insert:table', tableConverter );
 			dispatcher.on( 'insert:tr', tableConverter );
 			dispatcher.on( 'insert:td', tableConverter );

+ 5 - 5
packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import ModelConversionDispatcher from '../../src/conversion/modelconversiondispatcher';
+import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
 import Model from '../../src/model/model';
 import ModelText from '../../src/model/text';
 import ModelElement from '../../src/model/element';
@@ -12,13 +12,13 @@ import ModelPosition from '../../src/model/position';
 
 import ViewContainerElement from '../../src/view/containerelement';
 
-describe( 'ModelConversionDispatcher', () => {
+describe( 'DowncastDispatcher', () => {
 	let dispatcher, doc, root, differStub, model;
 
 	beforeEach( () => {
 		model = new Model();
 		doc = model.document;
-		dispatcher = new ModelConversionDispatcher( model );
+		dispatcher = new DowncastDispatcher( model );
 		root = doc.createRoot();
 
 		differStub = {
@@ -29,9 +29,9 @@ describe( 'ModelConversionDispatcher', () => {
 	} );
 
 	describe( 'constructor()', () => {
-		it( 'should create ModelConversionDispatcher with given api', () => {
+		it( 'should create DowncastDispatcher with given api', () => {
 			const apiObj = {};
-			const dispatcher = new ModelConversionDispatcher( model, { apiObj } );
+			const dispatcher = new DowncastDispatcher( model, { apiObj } );
 
 			expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
 		} );

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

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

+ 783 - 0
packages/ckeditor5-engine/tests/conversion/upcast-converters.js

@@ -0,0 +1,783 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Conversion from '../../src/conversion/conversion';
+import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
+
+import ViewContainerElement from '../../src/view/containerelement';
+import ViewDocumentFragment from '../../src/view/documentfragment';
+import ViewText from '../../src/view/text';
+import ViewUIElement from '../../src/view/uielement';
+import ViewAttributeElement from '../../src/view/attributeelement';
+
+import Model from '../../src/model/model';
+import ModelDocumentFragment from '../../src/model/documentfragment';
+import ModelElement from '../../src/model/element';
+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 { stringify } from '../../src/dev-utils/model';
+
+describe( 'upcast-helpers', () => {
+	let dispatcher, model, schema, conversion;
+
+	beforeEach( () => {
+		model = new Model();
+
+		schema = model.schema;
+
+		schema.extend( '$text', {
+			allowIn: '$root'
+		} );
+
+		schema.register( '$marker', {
+			inheritAllFrom: '$block'
+		} );
+
+		schema.register( 'paragraph', {
+			inheritAllFrom: '$block'
+		} );
+
+		schema.extend( '$text', {
+			allowAttributes: [ 'bold' ]
+		} );
+
+		dispatcher = new UpcastDispatcher( model, { schema } );
+		dispatcher.on( 'text', convertText() );
+		dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+		dispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
+
+		conversion = new Conversion();
+		conversion.register( 'upcast', [ dispatcher ] );
+	} );
+
+	describe( 'upcastElementToElement', () => {
+		it( 'config.view is a string', () => {
+			const helper = upcastElementToElement( { view: 'p', model: 'paragraph' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			schema.register( 'p', {
+				inheritAllFrom: '$block'
+			} );
+
+			const helperA = upcastElementToElement( { view: 'p', model: 'p' } );
+			const helperB = upcastElementToElement( { view: 'p', model: 'paragraph' }, 'high' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
+		} );
+
+		it( 'config.view is an object', () => {
+			schema.register( 'fancyParagraph', {
+				inheritAllFrom: '$block'
+			} );
+
+			const helperParagraph = upcastElementToElement( { view: 'p', model: 'paragraph' } );
+			const helperFancy = upcastElementToElement( {
+				view: {
+					name: 'p',
+					class: 'fancy'
+				},
+				model: 'fancyParagraph'
+			}, 'high' );
+
+			conversion.for( 'upcast' ).add( helperParagraph ).add( helperFancy );
+
+			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
+			expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<fancyParagraph></fancyParagraph>' );
+		} );
+
+		it( 'config.model is element instance', () => {
+			schema.extend( 'paragraph', {
+				allowAttributes: [ 'fancy' ]
+			} );
+
+			const helper = upcastElementToElement( {
+				view: {
+					name: 'p',
+					class: 'fancy'
+				},
+				model: new ModelElement( 'paragraph', { fancy: true } )
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<paragraph fancy="true"></paragraph>' );
+		} );
+
+		it( 'config.model is a function', () => {
+			schema.register( 'heading', {
+				inheritAllFrom: '$block',
+				allowAttributes: [ 'level' ]
+			} );
+
+			const helper = upcastElementToElement( {
+				view: {
+					name: 'p',
+					class: 'heading'
+				},
+				model: viewElement => new ModelElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } )
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult( new ViewContainerElement( 'p', { class: 'heading', 'data-level': 2 } ), '<heading level="2"></heading>' );
+		} );
+
+		it( 'should fire conversion of the element children', () => {
+			const helper = upcastElementToElement( { view: 'p', model: 'paragraph' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), '<paragraph>foo</paragraph>' );
+		} );
+
+		it( 'should not insert a model element if it is not allowed by schema', () => {
+			const helper = upcastElementToElement( { view: 'h2', model: 'heading' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult( new ViewContainerElement( 'h2' ), '' );
+		} );
+
+		it( 'should auto-break elements', () => {
+			schema.register( 'heading', {
+				inheritAllFrom: '$block'
+			} );
+
+			const helperParagraph = upcastElementToElement( { view: 'p', model: 'paragraph' } );
+			const helperHeading = upcastElementToElement( { view: 'h2', model: 'heading' } );
+
+			conversion.for( 'upcast' ).add( helperParagraph ).add( helperHeading );
+
+			expectResult(
+				new ViewContainerElement( 'p', null, [
+					new ViewText( 'Foo' ),
+					new ViewContainerElement( 'h2', null, new ViewText( 'Xyz' ) ),
+					new ViewText( 'Bar' )
+				] ),
+				'<paragraph>Foo</paragraph><heading>Xyz</heading><paragraph>Bar</paragraph>'
+			);
+		} );
+
+		it( 'should not do anything if returned model element is null', () => {
+			const helperA = upcastElementToElement( { view: 'p', model: 'paragraph' } );
+			const helperB = upcastElementToElement( { view: 'p', model: () => null }, 'high' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
+		} );
+	} );
+
+	describe( 'upcastElementToAttribute', () => {
+		it( 'config.view is string', () => {
+			const helper = upcastElementToAttribute( { view: 'strong', model: 'bold' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
+				'<$text bold="true">foo</$text>'
+			);
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = upcastElementToAttribute( { view: 'strong', model: 'strong' } );
+			const helperB = upcastElementToAttribute( { view: 'strong', model: 'bold' }, 'high' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult(
+				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
+				'<$text bold="true">foo</$text>'
+			);
+		} );
+
+		it( 'config.view is an object', () => {
+			const helper = upcastElementToAttribute( {
+				view: {
+					name: 'span',
+					class: 'bold'
+				},
+				model: 'bold'
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'span', { class: 'bold' }, new ViewText( 'foo' ) ),
+				'<$text bold="true">foo</$text>'
+			);
+		} );
+
+		it( 'model attribute value is given', () => {
+			schema.extend( '$text', {
+				allowAttributes: [ 'styled' ]
+			} );
+
+			const helper = upcastElementToAttribute( {
+				view: {
+					name: 'span',
+					class: [ 'styled', 'styled-dark' ]
+				},
+				model: {
+					key: 'styled',
+					value: 'dark'
+				}
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'span', { class: 'styled styled-dark' }, new ViewText( 'foo' ) ),
+				'<$text styled="dark">foo</$text>'
+			);
+		} );
+
+		it( 'model attribute value is a function', () => {
+			schema.extend( '$text', {
+				allowAttributes: [ 'fontSize' ]
+			} );
+
+			const helper = upcastElementToAttribute( {
+				view: {
+					name: 'span',
+					style: {
+						'font-size': /[\s\S]+/
+					}
+				},
+				model: {
+					key: 'fontSize',
+					value: viewElement => {
+						const fontSize = viewElement.getStyle( 'font-size' );
+						const value = fontSize.substr( 0, fontSize.length - 2 );
+
+						if ( value <= 10 ) {
+							return 'small';
+						} else if ( value > 12 ) {
+							return 'big';
+						}
+
+						return null;
+					}
+				}
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'span', { style: 'font-size:9px' }, new ViewText( 'foo' ) ),
+				'<$text fontSize="small">foo</$text>'
+			);
+
+			expectResult(
+				new ViewAttributeElement( 'span', { style: 'font-size:12px' }, new ViewText( 'foo' ) ),
+				'foo'
+			);
+
+			expectResult(
+				new ViewAttributeElement( 'span', { style: 'font-size:14px' }, new ViewText( 'foo' ) ),
+				'<$text fontSize="big">foo</$text>'
+			);
+		} );
+
+		it( 'should not set an attribute if it is not allowed by schema', () => {
+			const helper = upcastElementToAttribute( { view: 'em', model: 'italic' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'em', null, new ViewText( 'foo' ) ),
+				'foo'
+			);
+		} );
+
+		it( 'should not do anything if returned model attribute is null', () => {
+			const helperA = upcastElementToAttribute( { view: 'strong', model: 'bold' } );
+			const helperB = upcastElementToAttribute( {
+				view: 'strong',
+				model: {
+					key: 'bold',
+					value: () => null
+				}
+			}, 'high' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult(
+				new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ),
+				'<$text bold="true">foo</$text>'
+			);
+		} );
+	} );
+
+	describe( 'upcastAttributeToAttribute', () => {
+		beforeEach( () => {
+			conversion.for( 'upcast' ).add( upcastElementToElement( { view: 'img', model: 'image' } ) );
+
+			schema.register( 'image', {
+				inheritAllFrom: '$block'
+			} );
+		} );
+
+		it( 'config.view is a string', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'source' ]
+			} );
+
+			const helper = upcastAttributeToAttribute( { view: 'src', model: 'source' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
+				'<image source="foo.jpg"></image>'
+			);
+		} );
+
+		it( 'config.view has only key set', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'source' ]
+			} );
+
+			const helper = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
+				'<image source="foo.jpg"></image>'
+			);
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'src', 'source' ]
+			} );
+
+			const helperA = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'src' } );
+			const helperB = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
+				'<image source="foo.jpg"></image>'
+			);
+		} );
+
+		it( 'config.view has value set', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'styled' ]
+			} );
+
+			const helper = upcastAttributeToAttribute( {
+				view: {
+					key: 'data-style',
+					value: /[\s\S]*/
+				},
+				model: 'styled'
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { 'data-style': 'dark' } ),
+				'<image styled="dark"></image>'
+			);
+		} );
+
+		it( 'model attribute value is a string', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'styled' ]
+			} );
+
+			const helper = upcastAttributeToAttribute( {
+				view: {
+					name: 'img',
+					key: 'class',
+					value: 'styled-dark'
+				},
+				model: {
+					key: 'styled',
+					value: 'dark'
+				}
+			} );
+
+			conversion.for( 'upcast' )
+				.add( helper )
+				.add( upcastElementToElement( { view: 'p', model: 'paragraph' } ) );
+
+			expectResult(
+				new ViewContainerElement( 'img', { class: 'styled-dark' } ),
+				'<image styled="dark"></image>'
+			);
+
+			expectResult(
+				new ViewContainerElement( 'img', { class: 'styled-xxx' } ),
+				'<image></image>'
+			);
+
+			expectResult(
+				new ViewContainerElement( 'p', { class: 'styled-dark' } ),
+				'<paragraph></paragraph>'
+			);
+		} );
+
+		it( 'model attribute value is a function', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'styled' ]
+			} );
+
+			const helper = upcastAttributeToAttribute( {
+				view: {
+					key: 'class',
+					value: /styled-[\S]+/
+				},
+				model: {
+					key: 'styled',
+					value: viewElement => {
+						const regexp = /styled-([\S]+)/;
+						const match = viewElement.getAttribute( 'class' ).match( regexp );
+
+						return match[ 1 ];
+					}
+				}
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { 'class': 'styled-dark' } ),
+				'<image styled="dark"></image>'
+			);
+		} );
+
+		it( 'should not set an attribute if it is not allowed by schema', () => {
+			const helper = upcastAttributeToAttribute( { view: 'src', model: 'source' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { src: 'foo.jpg' } ),
+				'<image></image>'
+			);
+		} );
+
+		it( 'should not do anything if returned model attribute is null', () => {
+			schema.extend( 'image', {
+				allowAttributes: [ 'styled' ]
+			} );
+
+			const helperA = upcastAttributeToAttribute( {
+				view: {
+					key: 'class',
+					value: 'styled'
+				},
+				model: {
+					key: 'styled',
+					value: true
+				}
+			} );
+
+			const helperB = upcastAttributeToAttribute( {
+				view: {
+					key: 'class',
+					value: 'styled'
+				},
+				model: {
+					key: 'styled',
+					value: () => null
+				}
+			} );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			expectResult(
+				new ViewAttributeElement( 'img', { class: 'styled' } ),
+				'<image styled="true"></image>'
+			);
+		} );
+	} );
+
+	describe( 'upcastElementToMarker', () => {
+		it( 'config.view is a string', () => {
+			const helper = upcastElementToMarker( { view: 'marker-search', model: 'search' } );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			const frag = new ViewDocumentFragment( [
+				new ViewText( 'fo' ),
+				new ViewUIElement( 'marker-search' ),
+				new ViewText( 'oba' ),
+				new ViewUIElement( 'marker-search' ),
+				new ViewText( 'r' )
+			] );
+
+			const marker = { name: 'search', start: [ 2 ], end: [ 5 ] };
+
+			expectResult( frag, 'foobar', marker );
+		} );
+
+		it( 'can be overwritten using priority', () => {
+			const helperA = upcastElementToMarker( { view: 'marker-search', model: 'search-result' } );
+			const helperB = upcastElementToMarker( { view: 'marker-search', model: 'search' }, 'high' );
+
+			conversion.for( 'upcast' ).add( helperA ).add( helperB );
+
+			const frag = new ViewDocumentFragment( [
+				new ViewText( 'fo' ),
+				new ViewUIElement( 'marker-search' ),
+				new ViewText( 'oba' ),
+				new ViewUIElement( 'marker-search' ),
+				new ViewText( 'r' )
+			] );
+
+			const marker = { name: 'search', start: [ 2 ], end: [ 5 ] };
+
+			expectResult( frag, 'foobar', marker );
+		} );
+
+		it( 'config.view is an object', () => {
+			const helper = upcastElementToMarker( {
+				view: {
+					name: 'span',
+					'data-marker': 'search'
+				},
+				model: 'search'
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			const frag = new ViewDocumentFragment( [
+				new ViewText( 'f' ),
+				new ViewUIElement( 'span', { 'data-marker': 'search' } ),
+				new ViewText( 'oob' ),
+				new ViewUIElement( 'span', { 'data-marker': 'search' } ),
+				new ViewText( 'ar' )
+			] );
+
+			const marker = { name: 'search', start: [ 1 ], end: [ 4 ] };
+
+			expectResult( frag, 'foobar', marker );
+		} );
+
+		it( 'config.model is a function', () => {
+			const helper = upcastElementToMarker( {
+				view: 'comment',
+				model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
+			} );
+
+			conversion.for( 'upcast' ).add( helper );
+
+			const frag = new ViewDocumentFragment( [
+				new ViewText( 'foo' ),
+				new ViewUIElement( 'comment', { 'data-comment-id': 4 } ),
+				new ViewText( 'b' ),
+				new ViewUIElement( 'comment', { 'data-comment-id': 4 } ),
+				new ViewText( 'ar' )
+			] );
+
+			const marker = { name: 'comment:4', start: [ 3 ], end: [ 4 ] };
+
+			expectResult( frag, 'foobar', marker );
+		} );
+	} );
+
+	function expectResult( viewToConvert, modelString, marker ) {
+		const model = dispatcher.convert( viewToConvert );
+
+		if ( marker ) {
+			expect( model.markers.has( marker.name ) ).to.be.true;
+
+			const convertedMarker = model.markers.get( marker.name );
+
+			expect( convertedMarker.start.path ).to.deep.equal( marker.start );
+			expect( convertedMarker.end.path ).to.deep.equal( marker.end );
+		}
+
+		expect( stringify( model ) ).to.equal( modelString );
+	}
+} );
+
+describe( 'upcast-converters', () => {
+	let dispatcher, schema, context, model;
+
+	beforeEach( () => {
+		model = new Model();
+		schema = model.schema;
+
+		schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+		schema.extend( '$text', { allowIn: '$root' } );
+
+		context = [ '$root' ];
+
+		dispatcher = new UpcastDispatcher( model, { schema } );
+	} );
+
+	describe( 'convertText()', () => {
+		it( 'should return converter converting ViewText to ModelText', () => {
+			const viewText = new ViewText( 'foobar' );
+
+			dispatcher.on( 'text', convertText() );
+
+			const conversionResult = dispatcher.convert( viewText );
+
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
+			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foobar' );
+		} );
+
+		it( 'should not convert already consumed texts', () => {
+			const viewText = new ViewText( 'foofuckbafuckr' );
+
+			// Default converter for elements. Returns just converted children. Added with lowest priority.
+			dispatcher.on( 'text', convertText(), { priority: 'lowest' } );
+			// Added with normal priority. Should make the above converter not fire.
+			dispatcher.on( 'text', ( evt, data, conversionApi ) => {
+				if ( conversionApi.consumable.consume( data.viewItem ) ) {
+					const text = conversionApi.writer.createText( data.viewItem.data.replace( /fuck/gi, '****' ) );
+					conversionApi.writer.insert( text, data.modelCursor );
+					data.modelRange = ModelRange.createFromPositionAndShift( data.modelCursor, text.offsetSize );
+					data.modelCursor = data.modelRange.end;
+				}
+			} );
+
+			const conversionResult = dispatcher.convert( viewText, context );
+
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
+			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foo****ba****r' );
+		} );
+
+		it( 'should not convert text if it is wrong with schema', () => {
+			schema.addChildCheck( ( ctx, childDef ) => {
+				if ( childDef.name == '$text' && ctx.endsWith( '$root' ) ) {
+					return false;
+				}
+			} );
+
+			const viewText = new ViewText( 'foobar' );
+			dispatcher.on( 'text', convertText() );
+
+			let conversionResult = dispatcher.convert( viewText, context );
+
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.childCount ).to.equal( 0 );
+
+			conversionResult = dispatcher.convert( viewText, [ '$block' ] );
+
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.childCount ).to.equal( 1 );
+			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
+			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foobar' );
+		} );
+
+		it( 'should support unicode', () => {
+			const viewText = new ViewText( 'நிலைக்கு' );
+
+			dispatcher.on( 'text', convertText() );
+
+			const conversionResult = dispatcher.convert( viewText, context );
+
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
+			expect( conversionResult.getChild( 0 ).data ).to.equal( 'நிலைக்கு' );
+		} );
+	} );
+
+	describe( 'convertToModelFragment()', () => {
+		it( 'should return converter converting whole ViewDocumentFragment to ModelDocumentFragment', () => {
+			const viewFragment = new ViewDocumentFragment( [
+				new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ),
+				new ViewText( 'bar' )
+			] );
+
+			// To get any meaningful results we have to actually convert something.
+			dispatcher.on( 'text', convertText() );
+			// This way P element won't be converted per-se but will fire converting it's children.
+			dispatcher.on( 'element', convertToModelFragment() );
+			dispatcher.on( 'documentFragment', convertToModelFragment() );
+
+			const conversionResult = dispatcher.convert( viewFragment, context );
+
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.maxOffset ).to.equal( 6 );
+			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foobar' );
+		} );
+
+		it( 'should not convert already consumed (converted) changes', () => {
+			const viewP = new ViewContainerElement( 'p', null, new ViewText( 'foo' ) );
+
+			// To get any meaningful results we have to actually convert something.
+			dispatcher.on( 'text', convertText() );
+			// Default converter for elements. Returns just converted children. Added with lowest priority.
+			dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+			// Added with normal priority. Should make the above converter not fire.
+			dispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
+				if ( conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
+					const paragraph = conversionApi.writer.createElement( 'paragraph' );
+
+					conversionApi.writer.insert( paragraph, data.modelCursor );
+					conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( paragraph ) );
+
+					data.modelRange = ModelRange.createOn( paragraph );
+					data.modelCursor = data.modelRange.end;
+				}
+			} );
+
+			const conversionResult = dispatcher.convert( viewP, context );
+
+			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
+			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement );
+			expect( conversionResult.getChild( 0 ).name ).to.equal( 'paragraph' );
+			expect( conversionResult.getChild( 0 ).maxOffset ).to.equal( 3 );
+			expect( conversionResult.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
+		} );
+
+		it( 'should forward correct modelCursor', () => {
+			const spy = sinon.spy();
+			const view = new ViewDocumentFragment( [
+				new ViewContainerElement( 'div', null, [ new ViewText( 'abc' ), new ViewContainerElement( 'foo' ) ] ),
+				new ViewContainerElement( 'bar' )
+			] );
+			const position = ModelPosition.createAt( new ModelElement( 'element' ) );
+
+			dispatcher.on( 'documentFragment', convertToModelFragment() );
+			dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+			dispatcher.on( 'element:foo', ( evt, data ) => {
+				// Be sure that current cursor is not the same as custom.
+				expect( data.modelCursor ).to.not.equal( position );
+				// Set custom cursor as a result of docFrag last child conversion.
+				// This cursor should be forwarded by a documentFragment converter.
+				data.modelCursor = position;
+				// Be sure that callback was fired.
+				spy();
+			} );
+
+			dispatcher.on( 'element:bar', ( evt, data ) => {
+				expect( data.modelCursor ).to.equal( position );
+				spy();
+			} );
+
+			dispatcher.convert( view );
+
+			sinon.assert.calledTwice( spy );
+		} );
+	} );
+} );

+ 1 - 1
packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js

@@ -11,7 +11,7 @@ import createViewRoot from '../view/_utils/createroot';
 import Model from '../../src/model/model';
 
 import Mapper from '../../src/conversion/mapper';
-import { convertSelectionChange } from '../../src/conversion/view-selection-to-model-converters';
+import { convertSelectionChange } from '../../src/conversion/upcast-selection-converters';
 
 import { setData as modelSetData, getData as modelGetData } from '../../src/dev-utils/model';
 import { setData as viewSetData } from '../../src/dev-utils/view';

+ 8 - 8
packages/ckeditor5-engine/tests/conversion/viewconversiondispatcher.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
+import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
 import ViewContainerElement from '../../src/view/containerelement';
 import ViewDocumentFragment from '../../src/view/documentfragment';
 import ViewText from '../../src/view/text';
@@ -20,7 +20,7 @@ import ModelWriter from '../../src/model/writer';
 import first from '@ckeditor/ckeditor5-utils/src/first';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
-describe( 'ViewConversionDispatcher', () => {
+describe( 'UpcastDispatcher', () => {
 	let model;
 
 	beforeEach( () => {
@@ -28,9 +28,9 @@ describe( 'ViewConversionDispatcher', () => {
 	} );
 
 	describe( 'constructor()', () => {
-		it( 'should create ViewConversionDispatcher with passed api', () => {
+		it( 'should create UpcastDispatcher with passed api', () => {
 			const apiObj = {};
-			const dispatcher = new ViewConversionDispatcher( model, { apiObj } );
+			const dispatcher = new UpcastDispatcher( model, { apiObj } );
 
 			expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
 			expect( dispatcher.conversionApi ).to.have.property( 'convertItem' ).that.is.instanceof( Function );
@@ -39,7 +39,7 @@ describe( 'ViewConversionDispatcher', () => {
 		} );
 
 		it( 'should have properties', () => {
-			const dispatcher = new ViewConversionDispatcher( model );
+			const dispatcher = new UpcastDispatcher( model );
 
 			expect( dispatcher._removeIfEmpty ).to.instanceof( Set );
 		} );
@@ -49,7 +49,7 @@ describe( 'ViewConversionDispatcher', () => {
 		let dispatcher;
 
 		beforeEach( () => {
-			dispatcher = new ViewConversionDispatcher( model );
+			dispatcher = new UpcastDispatcher( model );
 		} );
 
 		it( 'should create api for current conversion process', () => {
@@ -335,7 +335,7 @@ describe( 'ViewConversionDispatcher', () => {
 		} );
 
 		it( 'should convert according to given context', () => {
-			dispatcher = new ViewConversionDispatcher( model, { schema: model.schema } );
+			dispatcher = new UpcastDispatcher( model, { schema: model.schema } );
 
 			const spy = sinon.spy();
 			const viewElement = new ViewContainerElement( 'third' );
@@ -397,7 +397,7 @@ describe( 'ViewConversionDispatcher', () => {
 			// Put nodes to documentFragment, this will mock root element and makes possible to create range on them.
 			rootMock = new ModelDocumentFragment( [ modelP, modelText ] );
 
-			dispatcher = new ViewConversionDispatcher( model, { schema: model.schema } );
+			dispatcher = new UpcastDispatcher( model, { schema: model.schema } );
 
 			dispatcher.on( 'element:p', ( evt, data ) => {
 				spyP();

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

@@ -1,185 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewDocumentFragment from '../../src/view/documentfragment';
-import ViewText from '../../src/view/text';
-
-import Model from '../../src/model/model';
-import ModelDocumentFragment from '../../src/model/documentfragment';
-import ModelElement from '../../src/model/element';
-import ModelText from '../../src/model/text';
-import ModelRange from '../../src/model/range';
-import ModelPosition from '../../src/model/position';
-
-import { convertToModelFragment, convertText } from '../../src/conversion/view-to-model-converters';
-
-describe( 'view-to-model-converters', () => {
-	let dispatcher, schema, context, model;
-
-	beforeEach( () => {
-		model = new Model();
-		schema = model.schema;
-
-		schema.register( 'paragraph', { inheritAllFrom: '$block' } );
-		schema.extend( '$text', { allowIn: '$root' } );
-
-		context = [ '$root' ];
-
-		dispatcher = new ViewConversionDispatcher( model, { schema } );
-	} );
-
-	describe( 'convertText()', () => {
-		it( 'should return converter converting ViewText to ModelText', () => {
-			const viewText = new ViewText( 'foobar' );
-
-			dispatcher.on( 'text', convertText() );
-
-			const conversionResult = dispatcher.convert( viewText );
-
-			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
-			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
-			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foobar' );
-		} );
-
-		it( 'should not convert already consumed texts', () => {
-			const viewText = new ViewText( 'foofuckbafuckr' );
-
-			// Default converter for elements. Returns just converted children. Added with lowest priority.
-			dispatcher.on( 'text', convertText(), { priority: 'lowest' } );
-			// Added with normal priority. Should make the above converter not fire.
-			dispatcher.on( 'text', ( evt, data, conversionApi ) => {
-				if ( conversionApi.consumable.consume( data.viewItem ) ) {
-					const text = conversionApi.writer.createText( data.viewItem.data.replace( /fuck/gi, '****' ) );
-					conversionApi.writer.insert( text, data.modelCursor );
-					data.modelRange = ModelRange.createFromPositionAndShift( data.modelCursor, text.offsetSize );
-					data.modelCursor = data.modelRange.end;
-				}
-			} );
-
-			const conversionResult = dispatcher.convert( viewText, context );
-
-			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
-			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
-			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foo****ba****r' );
-		} );
-
-		it( 'should not convert text if it is wrong with schema', () => {
-			schema.addChildCheck( ( ctx, childDef ) => {
-				if ( childDef.name == '$text' && ctx.endsWith( '$root' ) ) {
-					return false;
-				}
-			} );
-
-			const viewText = new ViewText( 'foobar' );
-			dispatcher.on( 'text', convertText() );
-
-			let conversionResult = dispatcher.convert( viewText, context );
-
-			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
-			expect( conversionResult.childCount ).to.equal( 0 );
-
-			conversionResult = dispatcher.convert( viewText, [ '$block' ] );
-
-			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
-			expect( conversionResult.childCount ).to.equal( 1 );
-			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
-			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foobar' );
-		} );
-
-		it( 'should support unicode', () => {
-			const viewText = new ViewText( 'நிலைக்கு' );
-
-			dispatcher.on( 'text', convertText() );
-
-			const conversionResult = dispatcher.convert( viewText, context );
-
-			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
-			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
-			expect( conversionResult.getChild( 0 ).data ).to.equal( 'நிலைக்கு' );
-		} );
-	} );
-
-	describe( 'convertToModelFragment()', () => {
-		it( 'should return converter converting whole ViewDocumentFragment to ModelDocumentFragment', () => {
-			const viewFragment = new ViewDocumentFragment( [
-				new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ),
-				new ViewText( 'bar' )
-			] );
-
-			// To get any meaningful results we have to actually convert something.
-			dispatcher.on( 'text', convertText() );
-			// This way P element won't be converted per-se but will fire converting it's children.
-			dispatcher.on( 'element', convertToModelFragment() );
-			dispatcher.on( 'documentFragment', convertToModelFragment() );
-
-			const conversionResult = dispatcher.convert( viewFragment, context );
-
-			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
-			expect( conversionResult.maxOffset ).to.equal( 6 );
-			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foobar' );
-		} );
-
-		it( 'should not convert already consumed (converted) changes', () => {
-			const viewP = new ViewContainerElement( 'p', null, new ViewText( 'foo' ) );
-
-			// To get any meaningful results we have to actually convert something.
-			dispatcher.on( 'text', convertText() );
-			// Default converter for elements. Returns just converted children. Added with lowest priority.
-			dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
-			// Added with normal priority. Should make the above converter not fire.
-			dispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
-				if ( conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
-					const paragraph = conversionApi.writer.createElement( 'paragraph' );
-
-					conversionApi.writer.insert( paragraph, data.modelCursor );
-					conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( paragraph ) );
-
-					data.modelRange = ModelRange.createOn( paragraph );
-					data.modelCursor = data.modelRange.end;
-				}
-			} );
-
-			const conversionResult = dispatcher.convert( viewP, context );
-
-			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
-			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement );
-			expect( conversionResult.getChild( 0 ).name ).to.equal( 'paragraph' );
-			expect( conversionResult.getChild( 0 ).maxOffset ).to.equal( 3 );
-			expect( conversionResult.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
-		} );
-
-		it( 'should forward correct modelCursor', () => {
-			const spy = sinon.spy();
-			const view = new ViewDocumentFragment( [
-				new ViewContainerElement( 'div', null, [ new ViewText( 'abc' ), new ViewContainerElement( 'foo' ) ] ),
-				new ViewContainerElement( 'bar' )
-			] );
-			const position = ModelPosition.createAt( new ModelElement( 'element' ) );
-
-			dispatcher.on( 'documentFragment', convertToModelFragment() );
-			dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
-			dispatcher.on( 'element:foo', ( evt, data ) => {
-				// Be sure that current cursor is not the same as custom.
-				expect( data.modelCursor ).to.not.equal( position );
-				// Set custom cursor as a result of docFrag last child conversion.
-				// This cursor should be forwarded by a documentFragment converter.
-				data.modelCursor = position;
-				// Be sure that callback was fired.
-				spy();
-			} );
-
-			dispatcher.on( 'element:bar', ( evt, data ) => {
-				expect( data.modelCursor ).to.equal( position );
-				spy();
-			} );
-
-			dispatcher.convert( view );
-
-			sinon.assert.calledTwice( spy );
-		} );
-	} );
-} );

+ 28 - 20
packages/ckeditor5-engine/tests/manual/highlight.js

@@ -5,6 +5,19 @@
 
 /* global console, window, document */
 
+import ModelRange from '../../src/model/range';
+import ViewContainerElement from '../../src/view/containerelement';
+import ViewText from '../../src/view/text';
+
+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';
@@ -15,12 +28,6 @@ import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 import List from '@ckeditor/ckeditor5-list/src/list';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
-import buildModelConverter from '../../src/conversion/buildmodelconverter';
-import buildViewConverter from '../../src/conversion/buildviewconverter';
-import ModelRange from '../../src/model/range';
-import ModelElement from '../../src/model/element';
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewText from '../../src/view/text';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
@@ -34,7 +41,6 @@ class FancyWidget extends Plugin {
 		const editor = this.editor;
 		const schema = editor.model.schema;
 		const data = editor.data;
-		const editing = editor.editing;
 
 		// Configure schema.
 		schema.register( 'fancywidget', {
@@ -42,19 +48,19 @@ class FancyWidget extends Plugin {
 		} );
 		schema.extend( 'fancywidget', { allowIn: '$root' } );
 
-		// Build converter from model to view for editing pipeline.
-		buildModelConverter().for( editing.modelToView )
-			.fromElement( 'fancywidget' )
-			.toElement( () => {
+		downcastElementToElement( {
+			model: 'fancywidget',
+			view: () => {
 				const widgetElement = new ViewContainerElement( 'figure', { class: 'fancy-widget' }, new ViewText( 'widget' ) );
 
 				return toWidget( widgetElement );
-			} );
+			}
+		} )( data.downcastDispatcher );
 
-		// Build converter from view element to model element for data pipeline.
-		buildViewConverter().for( data.viewToModel )
-			.fromElement( 'figure' )
-			.toElement( () => new ModelElement( 'fancywidget' ) );
+		upcastElementToElement( {
+			view: 'figure',
+			model: 'fancywidget'
+		} )( data.upcastDispatcher );
 	}
 }
 
@@ -65,10 +71,12 @@ ClassicEditor.create( global.document.querySelector( '#editor' ), {
 	.then( editor => {
 		window.editor = editor;
 
-		buildModelConverter()
-			.for( editor.editing.modelToView )
-			.fromMarker( 'marker' )
-			.toHighlight( data => ( { class: 'highlight-' + data.markerName.split( ':' )[ 1 ] } ) );
+		downcastMarkerToHighlight( {
+			model: 'marker',
+			view: data => ( {
+				class: 'highlight-' + data.markerName.split( ':' )[ 1 ]
+			} )
+		} );
 
 		document.getElementById( 'add-marker-yellow' ).addEventListener( 'mousedown', evt => {
 			addMarker( editor, 'yellow' );

+ 9 - 5
packages/ckeditor5-engine/tests/manual/markers.js

@@ -15,7 +15,10 @@ 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 buildModelConverter from '../../src/conversion/buildmodelconverter';
+import {
+	downcastMarkerToHighlight
+} from '../../src/conversion/downcast-converters';
+
 import Position from '../../src/model/position';
 import Range from '../../src/model/range';
 
@@ -32,16 +35,17 @@ ClassicEditor
 		window.editor = editor;
 		model = editor.model;
 
-		buildModelConverter().for( editor.editing.modelToView )
-			.fromMarker( 'highlight' )
-			.toHighlight( data => {
+		downcastMarkerToHighlight( {
+			model: 'highlight',
+			view: data => {
 				const color = data.markerName.split( ':' )[ 1 ];
 
 				return {
 					class: 'h-' + color,
 					priority: 1
 				};
-			} );
+			}
+		} );
 
 		window.document.getElementById( 'add-yellow' ).addEventListener( 'mousedown', e => {
 			e.preventDefault();

+ 34 - 20
packages/ckeditor5-engine/tests/manual/nestededitable.js

@@ -5,18 +5,24 @@
 
 /* global console */
 
+import {
+	upcastElementToElement
+} from '../../src/conversion/upcast-converters';
+
+import {
+	downcastElementToElement
+} from '../../src/conversion/downcast-converters';
+
+import ViewEditableElement from '../../src/view/editableelement';
+import { getData } from '../../src/dev-utils/model';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 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 buildModelConverter from '../../src/conversion/buildmodelconverter';
-import buildViewConverter from '../../src/conversion/buildviewconverter';
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewEditableElement from '../../src/view/editableelement';
-import { getData } from '../../src/dev-utils/model';
-import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
 import './nestededitable.css';
 
@@ -25,7 +31,6 @@ class NestedEditable extends Plugin {
 		const editor = this.editor;
 		const editing = editor.editing;
 		const viewDocument = editing.view;
-		const data = editor.data;
 		const schema = editor.model.schema;
 
 		schema.register( 'figure', {
@@ -38,17 +43,24 @@ class NestedEditable extends Plugin {
 			allowIn: [ 'figure', 'figcaption' ]
 		} );
 
-		buildModelConverter().for( data.modelToView, editing.modelToView )
-			.fromElement( 'figure' )
-			.toElement( () => new ViewContainerElement( 'figure', { contenteditable: 'false' } ) );
+		editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
+			model: 'figure',
+			view: {
+				name: 'figure',
+				attribute: {
+					contenteditable: 'false'
+				}
+			}
+		} ) );
 
-		buildViewConverter().for( data.viewToModel )
-			.fromElement( 'figure' )
-			.toElement( 'figure' );
+		editor.conversion.for( 'upcast' ).add( upcastElementToElement( {
+			model: 'figure',
+			view: 'figure'
+		} ) );
 
-		buildModelConverter().for( data.modelToView, editing.modelToView )
-			.fromElement( 'figcaption' )
-			.toElement( () => {
+		editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
+			model: 'figcaption',
+			view: () => {
 				const element = new ViewEditableElement( 'figcaption', { contenteditable: 'true' } );
 				element.document = viewDocument;
 
@@ -61,11 +73,13 @@ class NestedEditable extends Plugin {
 				} );
 
 				return element;
-			} );
+			}
+		} ) );
 
-		buildViewConverter().for( data.viewToModel )
-			.fromElement( 'figcaption' )
-			.toElement( 'figcaption' );
+		editor.conversion.for( 'upcast' ).add( upcastElementToElement( {
+			model: 'figcaption',
+			view: 'figcaption'
+		} ) );
 	}
 }
 

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

@@ -11,8 +11,13 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Range from '../../../../src/model/range';
 import LivePosition from '../../../../src/model/liveposition';
 
-import buildModelConverter from '../../../../src/conversion/buildmodelconverter';
-import buildViewConverter from '../../../../src/conversion/buildviewconverter';
+import {
+	upcastElementToAttribute
+} from '../../../../src/conversion/upcast-converters';
+
+import {
+	downcastAttributeToElement,
+} from '../../../../src/conversion/downcast-converters';
 
 import AttributeElement from '../../../../src/view/attributeelement';
 
@@ -24,21 +29,21 @@ import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 class Link extends Plugin {
 	init() {
 		const editor = this.editor;
-		const data = editor.data;
-		const editing = editor.editing;
 
 		// Allow bold attribute on all inline nodes.
 		editor.model.schema.extend( '$text', { allowAttributes: 'link' } );
 
-		// Build converter from model to view for data and editing pipelines.
-		buildModelConverter().for( data.modelToView, editing.modelToView )
-			.fromAttribute( 'link' )
-			.toElement( href => new AttributeElement( 'a', { href } ) );
+		editor.conversion.for( 'downcast' ).add( downcastAttributeToElement( 'link', {
+			view: attributeValue => new AttributeElement( 'a', { href: attributeValue } )
+		} ) );
 
-		// Build converter from view to model for data pipeline.
-		buildViewConverter().for( data.viewToModel )
-			.fromElement( 'a' )
-			.toAttribute( viewElement => ( { key: 'link', value: viewElement.getAttribute( 'href' ) } ) );
+		editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( {
+			view: 'a',
+			model: {
+				key: 'link',
+				value: viewElement => viewElement.getAttribute( 'href' )
+			}
+		} ) );
 	}
 }
 

+ 15 - 8
packages/ckeditor5-engine/tests/tickets/699.js

@@ -8,8 +8,13 @@
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
-import buildViewConverter from '../../src/conversion/buildviewconverter';
-import buildModelConverter from '../../src/conversion/buildmodelconverter';
+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';
@@ -49,11 +54,13 @@ function WidgetPlugin( editor ) {
 	} );
 	schema.extend( 'widget', { allowIn: '$root' } );
 
-	buildModelConverter().for( editor.data.modelToView, editor.editing.modelToView )
-		.fromElement( 'widget' )
-		.toElement( 'widget' );
+	editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
+		model: 'widget',
+		view: 'widget'
+	} ) );
 
-	buildViewConverter().for( editor.data.viewToModel )
-		.fromElement( 'widget' )
-		.toElement( 'widget' );
+	editor.conversion.for( 'upcast' ).add( upcastElementToElement( {
+		model: 'widget',
+		view: 'widget'
+	} ) );
 }

+ 8 - 0
packages/ckeditor5-engine/tests/view/element.js

@@ -41,6 +41,14 @@ describe( 'Element', () => {
 			expect( el.getAttribute( 'foo' ) ).to.equal( 'bar' );
 		} );
 
+		it( 'should stringify attributes', () => {
+			const el = new Element( 'p', { foo: true, bar: null, object: {} } );
+
+			expect( el.getAttribute( 'foo' ) ).to.equal( 'true' );
+			expect( el.getAttribute( 'bar' ) ).to.be.undefined;
+			expect( el.getAttribute( 'object' ) ).to.equal( '[object Object]' );
+		} );
+
 		it( 'should create element with children', () => {
 			const child = new Element( 'p', { foo: 'bar' } );
 			const parent = new Element( 'div', [], [ child ] );

+ 1 - 1
packages/ckeditor5-engine/tests/view/manual/uielement.js

@@ -51,7 +51,7 @@ class UIElementTestPlugin extends Plugin {
 		const editing = editor.editing;
 
 		// Add some UIElement to each paragraph.
-		editing.modelToView.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
+		editing.downcastDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
 			const viewP = conversionApi.mapper.toViewElement( data.item );
 			viewP.appendChildren( createEndingUIElement() );
 		}, { priority: 'lowest' } );