Browse Source

Merge branch 'master' into t/ckeditor5-utils/214

Piotrek Koszuliński 8 năm trước cách đây
mục cha
commit
d028ea8629
26 tập tin đã thay đổi với 1412 bổ sung1115 xóa
  1. 29 29
      packages/ckeditor5-engine/src/controller/datacontroller.js
  2. 6 6
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  3. 72 52
      packages/ckeditor5-engine/src/conversion/buildviewconverter.js
  4. 15 7
      packages/ckeditor5-engine/src/conversion/view-to-model-converters.js
  5. 271 133
      packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
  6. 3 3
      packages/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js
  7. 7 7
      packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc
  8. 13 13
      packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js
  9. 2 2
      packages/ckeditor5-engine/src/dataprocessor/htmlwriter.js
  10. 19 19
      packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js
  11. 35 22
      packages/ckeditor5-engine/src/dev-utils/model.js
  12. 3 3
      packages/ckeditor5-engine/src/model/differ.js
  13. 59 13
      packages/ckeditor5-engine/src/model/schema.js
  14. 44 2
      packages/ckeditor5-engine/src/model/utils/insertcontent.js
  15. 61 25
      packages/ckeditor5-engine/src/model/writer.js
  16. 2 2
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  17. 0 553
      packages/ckeditor5-engine/tests/conversion/advanced-converters.js
  18. 95 61
      packages/ckeditor5-engine/tests/conversion/buildviewconverter.js
  19. 18 18
      packages/ckeditor5-engine/tests/conversion/definition-based-converters.js
  20. 27 20
      packages/ckeditor5-engine/tests/conversion/view-to-model-converters.js
  21. 375 114
      packages/ckeditor5-engine/tests/conversion/viewconversiondispatcher.js
  22. 4 9
      packages/ckeditor5-engine/tests/dev-utils/model.js
  23. 17 0
      packages/ckeditor5-engine/tests/model/differ.js
  24. 140 0
      packages/ckeditor5-engine/tests/model/schema.js
  25. 57 0
      packages/ckeditor5-engine/tests/model/utils/insertcontent.js
  26. 38 2
      packages/ckeditor5-engine/tests/model/writer.js

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

@@ -26,7 +26,7 @@ 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 given:
+ * using the given:
  *
  * * {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor},
  * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher model to view} and
@@ -36,10 +36,11 @@ import ModelRange from '../model/range';
  */
 export default class DataController {
 	/**
-	 * Creates data controller instance.
+	 * Creates a data controller instance.
 	 *
 	 * @param {module:engine/model/model~Model} model Data model.
-	 * @param {module:engine/dataprocessor/dataprocessor~DataProcessor} [dataProcessor] Data processor which should used by the controller.
+	 * @param {module:engine/dataprocessor/dataprocessor~DataProcessor} [dataProcessor] Data processor that should be used
+	 * by the controller.
 	 */
 	constructor( model, dataProcessor ) {
 		/**
@@ -60,7 +61,7 @@ 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 data are converted. However, the mapper is defined as class property, because
+		 * 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.
 		 *
 		 * @member {module:engine/conversion/mapper~Mapper}
@@ -68,8 +69,8 @@ export default class DataController {
 		this.mapper = new Mapper();
 
 		/**
-		 * Model to view conversion dispatcher used by the {@link #get get method}.
-		 * To attach model to view converter to the data pipeline you need to add lister to this property:
+		 * 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 );
 		 *
@@ -86,8 +87,8 @@ export default class DataController {
 		this.modelToView.on( 'insert:$text', insertText(), { priority: 'lowest' } );
 
 		/**
-		 * View to model conversion dispatcher used by the {@link #set set method}.
-		 * To attach view to model converter to the data pipeline you need to add lister to this property:
+		 * 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 );
 		 *
@@ -113,7 +114,7 @@ export default class DataController {
 	}
 
 	/**
-	 * Returns model's data converted by the {@link #modelToView model to view converters} and
+	 * Returns the model's data converted by the {@link #modelToView model-to-view converters} and
 	 * formatted by the {@link #processor data processor}.
 	 *
 	 * @param {String} [rootName='main'] Root name.
@@ -127,11 +128,11 @@ 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 #modelToView model-to-view converters} and formatted by the
 	 * {@link #processor data processor}.
 	 *
 	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
-	 * Element which content will be stringified.
+	 * Element whose content will be stringified.
 	 * @returns {String} Output data.
 	 */
 	stringify( modelElementOrFragment ) {
@@ -145,11 +146,11 @@ 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 #modelToView model-to-view converters} to a
 	 * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}.
 	 *
 	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
-	 * Element or document fragment which content will be converted.
+	 * Element or document fragment whose content will be converted.
 	 * @returns {module:engine/view/documentfragment~DocumentFragment} Output view DocumentFragment.
 	 */
 	toView( modelElementOrFragment ) {
@@ -179,9 +180,9 @@ 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 #viewToModel view-to-model converters}.
 	 *
-	 * This method also creates a batch with all the changes applied. If all you need is to parse data use
+	 * This method also creates a batch with all the changes applied. If all you need is to parse data, use
 	 * the {@link #parse} method.
 	 *
 	 * @param {String} data Input data.
@@ -203,16 +204,16 @@ export default class DataController {
 	}
 
 	/**
-	 * Returns 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 the {@link #viewToModel view-to-model converters}.
 	 *
 	 * @see #set
 	 * @param {String} data Data to parse.
-	 * @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
-	 * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
+	 * @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}.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Parsed data.
 	 */
-	parse( data, context = '$root' ) {
+	parse( data, context = [ '$root' ] ) {
 		// data -> view
 		const viewDocumentFragment = this.processor.toView( data );
 
@@ -221,22 +222,21 @@ export default class DataController {
 	}
 
 	/**
-	 * Returns wrapped by {module:engine/model/documentfragment~DocumentFragment} result of the given
-	 * {@link module:engine/view/element~Element view element} or
+	 * 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}.
+	 * {@link #viewToModel view-to-model converters}, wrapped by {module:engine/model/documentfragment~DocumentFragment}.
 	 *
-	 * When marker elements were converted during conversion process then will be set as DocumentFragment's
+	 * 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}.
 	 *
 	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
-	 * Element or document fragment which content will be converted.
-	 * @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
-	 * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
+	 * 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}.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
 	 */
-	toModel( viewElementOrFragment, context = '$root' ) {
-		return this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } );
+	toModel( viewElementOrFragment, context = [ '$root' ] ) {
+		return this.viewToModel.convert( viewElementOrFragment, context );
 	}
 
 	/**

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

@@ -28,14 +28,14 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 /**
  * Controller for the editing pipeline. The editing pipeline controls {@link ~EditingController#model model} rendering,
- * including selection handling. It also creates {@link ~EditingController#view view document} which build a
- * browser-independent virtualization over the DOM elements. Editing controller also attach default converters.
+ * including selection handling. It also creates the {@link ~EditingController#view view document} which builds a
+ * browser-independent virtualization over the DOM elements. The editing controller also attaches default converters.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
 export default class EditingController {
 	/**
-	 * Creates editing controller instance.
+	 * Creates an editing controller instance.
 	 *
 	 * @param {module:engine/model/model~Model} model Editing model.
 	 */
@@ -57,7 +57,7 @@ export default class EditingController {
 		this.view = new ViewDocument();
 
 		/**
-		 * Mapper which describes model-view binding.
+		 * Mapper which describes the model-view binding.
 		 *
 		 * @readonly
 		 * @member {module:engine/conversion/mapper~Mapper}
@@ -65,9 +65,9 @@ export default class EditingController {
 		this.mapper = new Mapper();
 
 		/**
-		 * Model to view conversion dispatcher, which converts changes from the model to {@link #view the editing view}.
+		 * Model-to-view conversion dispatcher that converts changes from the model to {@link #view the editing view}.
 		 *
-		 * To attach model-to-view converter to the editing pipeline you need to add a listener to this dispatcher:
+		 * 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 );
 		 *

+ 72 - 52
packages/ckeditor5-engine/src/conversion/buildviewconverter.js

@@ -9,7 +9,8 @@
 
 import Matcher from '../view/matcher';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
+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
@@ -269,12 +270,12 @@ class ViewConverterBuilder {
 	 */
 	toElement( element ) {
 		function eventCallbackGen( from ) {
-			return ( evt, data, consumable, conversionApi ) => {
+			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.input );
+				const matchAll = from.matcher.matchAll( data.viewItem );
 
 				// If there is no match, this callback should not do anything.
 				if ( !matchAll ) {
@@ -284,38 +285,60 @@ class ViewConverterBuilder {
 				// 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.input, writer ) : writer.createElement( element );
+					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;
 					}
 
-					if ( !conversionApi.schema.checkChild( data.context, modelElement ) ) {
+					// When element was already consumed then skip it.
+					if ( !conversionApi.consumable.test( data.viewItem, from.consume || match.match ) ) {
 						continue;
 					}
 
-					// Try to consume appropriate values from consumable values list.
-					if ( !consumable.consume( data.input, from.consume || match.match ) ) {
+					// 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;
 					}
 
-					// If everything is fine, we are ready to start the conversion.
-					// Add newly created `modelElement` to the parents stack.
-					data.context.push( modelElement );
+					// Insert element on allowed position.
+					conversionApi.writer.insert( modelElement, splitResult.position );
 
-					// Convert children of converted view element and append them to `modelElement`.
-					const modelChildren = conversionApi.convertChildren( data.input, consumable, data );
+					// Convert children and insert to element.
+					const childrenResult = conversionApi.convertChildren( data.viewItem, Position.createAt( modelElement ) );
 
-					for ( const child of Array.from( modelChildren ) ) {
-						writer.append( child, 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 )
+					);
 
-					// Remove created `modelElement` from the parents stack.
-					data.context.pop();
+					// 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 );
 
-					// Add `modelElement` as a result.
-					data.output = modelElement;
+					// Otherwise just continue after inserted element.
+					} else {
+						data.modelCursor = data.modelRange.end;
+					}
 
 					// Prevent multiple conversion if there are other correct matches.
 					break;
@@ -345,10 +368,10 @@ class ViewConverterBuilder {
 	 */
 	toAttribute( keyOrCreator, value ) {
 		function eventCallbackGen( from ) {
-			return ( evt, data, consumable, conversionApi ) => {
+			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.input );
+				const matchAll = from.matcher.matchAll( data.viewItem );
 
 				// If there is no match, this callback should not do anything.
 				if ( !matchAll ) {
@@ -358,21 +381,22 @@ class ViewConverterBuilder {
 				// 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 ( !consumable.consume( data.input, from.consume || match.match ) ) {
+					if ( !conversionApi.consumable.consume( data.viewItem, from.consume || match.match ) ) {
 						continue;
 					}
 
-					// Since we are converting to attribute we need an output on which we will set the attribute.
-					// If the output is not created yet, we will create it.
-					if ( !data.output ) {
-						data.output = conversionApi.convertChildren( data.input, consumable, data );
+					// 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.input );
+						attribute = keyOrCreator( data.viewItem );
 
 						if ( !attribute ) {
 							return;
@@ -380,12 +404,16 @@ class ViewConverterBuilder {
 					} else {
 						attribute = {
 							key: keyOrCreator,
-							value: value ? value : data.input.getAttribute( from.attributeKey )
+							value: value ? value : data.viewItem.getAttribute( from.attributeKey )
 						};
 					}
 
-					// Set attribute on current `output`. `Schema` is checked inside this helper function.
-					setAttributeOn( data.output, attribute, data, conversionApi );
+					// 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;
@@ -431,12 +459,12 @@ class ViewConverterBuilder {
 	 */
 	toMarker( creator ) {
 		function eventCallbackGen( from ) {
-			return ( evt, data, consumable, conversionApi ) => {
+			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.input );
+				const matchAll = from.matcher.matchAll( data.viewItem );
 
 				// If there is no match, this callback should not do anything.
 				if ( !matchAll ) {
@@ -447,10 +475,10 @@ class ViewConverterBuilder {
 
 				// When creator is provided then create model element basing on creator function.
 				if ( creator instanceof Function ) {
-					modelElement = creator( data.input );
+					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.input.getAttribute( 'data-name' ) } );
+					modelElement = writer.createElement( '$marker', { 'data-name': data.viewItem.getAttribute( 'data-name' ) } );
 				}
 
 				// Check if model element is correct (has proper name and property).
@@ -463,11 +491,19 @@ class ViewConverterBuilder {
 				// 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 ( !consumable.consume( data.input, from.consume || match.match ) ) {
+					if ( !conversionApi.consumable.consume( data.viewItem, from.consume || match.match ) ) {
 						continue;
 					}
 
-					data.output = modelElement;
+					// 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;
@@ -504,22 +540,6 @@ class ViewConverterBuilder {
 	}
 }
 
-// Helper function that sets given attributes on given `module:engine/model/node~Node` or
-// `module:engine/model/documentfragment~DocumentFragment`.
-function setAttributeOn( toChange, attribute, data, conversionApi ) {
-	if ( isIterable( toChange ) ) {
-		for ( const node of toChange ) {
-			setAttributeOn( node, attribute, data, conversionApi );
-		}
-
-		return;
-	}
-
-	if ( conversionApi.schema.checkAttribute( toChange, attribute.key ) ) {
-		conversionApi.writer.setAttribute( attribute.key, attribute.value, toChange );
-	}
-}
-
 /**
  * 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

+ 15 - 7
packages/ckeditor5-engine/src/conversion/view-to-model-converters.js

@@ -3,6 +3,8 @@
  * 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}.
@@ -26,10 +28,11 @@
  * {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item.
  */
 export function convertToModelFragment() {
-	return ( evt, data, consumable, conversionApi ) => {
+	return ( evt, data, conversionApi ) => {
 		// Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
-		if ( !data.output && consumable.consume( data.input, { name: true } ) ) {
-			data.output = conversionApi.convertChildren( data.input, consumable, data );
+		if ( !data.modelRange && conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
+			data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
+			data.modelCursor = data.modelRange.end;
 		}
 	};
 }
@@ -40,10 +43,15 @@ export function convertToModelFragment() {
  * @returns {Function} {@link module:engine/view/text~Text View text} converter.
  */
 export function convertText() {
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( conversionApi.schema.checkChild( data.context, '$text' ) ) {
-			if ( consumable.consume( data.input ) ) {
-				data.output = conversionApi.writer.createText( data.input.data );
+	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;
 			}
 		}
 	};

+ 271 - 133
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -10,37 +10,30 @@
 import ViewConsumable from './viewconsumable';
 import ModelRange from '../model/range';
 import ModelPosition from '../model/position';
-import ModelTreeWalker from '../model/treewalker';
-import ModelNode from '../model/node';
-import ModelDocumentFragment from '../model/documentfragment';
+import { SchemaContext } from '../model/schema';
 
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 
 /**
  * `ViewConversionDispatcher` 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}.
+ * {@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.
  *
- * Each callback, as a first argument, is passed a special object `data` that has `input` and `output` properties.
- * `input` property contains {@link module:engine/view/node~Node view node} or
+ * Each callback, as a first 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. `output` property should be used to save the result
- * of conversion. Keep in mind that the `data` parameter is customizable and may contain other values - see
- * {@link ~ViewConversionDispatcher#convert}. It is also shared by reference by all callbacks
- * listening to given event. **Note**: in view to model conversion - `data` contains `context` property that is an array
- * of {@link module:engine/model/element~Element model elements}. These are model elements that will be the parent of currently
- * converted view item. `context` property is used in examples below.
- *
- * The second parameter passed to a callback is an instance of {@link module:engine/conversion/viewconsumable~ViewConsumable}. It stores
- * information about what parts of processed view item are still waiting to be handled. After a piece of view item
- * was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
+ * 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.
  *
  * The third parameter passed to a callback is an instance of {@link ~ViewConversionDispatcher}
  * which provides additional tools for converters.
@@ -48,37 +41,39 @@ import log from '@ckeditor/ckeditor5-utils/src/log';
  * Examples of providing callbacks for `ViewConversionDispatcher`:
  *
  *		// Converter for paragraphs (<p>).
- *		viewDispatcher.on( 'element:p', ( evt, data, consumable, conversionApi ) => {
- *			const paragraph = new ModelElement( 'paragraph' );
+ *		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 );
  *
- *			if ( conversionApi.schema.checkChild( data.context, paragraph ) ) {
- *				if ( !consumable.consume( data.input, { name: true } ) ) {
- *					// Before converting this paragraph's children we have to update their context by this paragraph.
- *					data.context.push( paragraph );
- *					const children = conversionApi.convertChildren( data.input, consumable, data );
- *					data.context.pop();
- *					paragraph.appendChildren( children );
- *					data.output = paragraph;
+ *					// Continue conversion just after paragraph.
+ *					data.modelCursor = data.modelRange.end;
  *				}
  *			}
  *		} );
  *
  *		// Converter for links (<a>).
- *		viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => {
- *			if ( consumable.consume( data.input, { name: true, attributes: [ 'href' ] } ) ) {
+ *		viewDispatcher.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 are not updating `context` property.
- *				data.output = conversionApi.convertChildren( data.input, consumable, data );
+ *				// This is why we need to convert only children.
+ *				const { modelRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
  *
- *				for ( let item of Range.createFrom( data.output ) ) {
- *					const schemaQuery = {
- *						name: item.name || '$text',
- *						attribute: 'link',
- *						inside: data.context
- *					};
- *
- *					if ( conversionApi.schema.checkAttribute( [ ...data.context, '$text' ], 'link' ) ) {
- *						item.setAttribute( 'link', data.input.getAttribute( 'href' ) );
+ *				for ( let item of modelRange.getItems() ) {
+ *					if ( conversionApi.schema.checkAttribute( item, 'linkHref' ) ) {
+ *						conversionApi.writer.setAttribute( 'linkHref', data.viewItem.getAttribute( 'href' ), item );
  *					}
  *				}
  *			}
@@ -116,6 +111,32 @@ export default class ViewConversionDispatcher {
 		 */
 		this._model = model;
 
+		/**
+		 * List of elements that will be checked after conversion process and if element in the list will be empty it
+		 * will be removed from conversion result.
+		 *
+		 * After conversion process list is cleared.
+		 *
+		 * @protected
+		 * @type {Set<module:engine/model/element~Element>}
+		 */
+		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.
+		 *
+		 * @private
+		 * @type {module:engine/model/position~Position|null}
+		 */
+		this._modelCursor = null;
+
 		/**
 		 * Interface passed by dispatcher to the events callbacks.
 		 *
@@ -123,10 +144,11 @@ export default class ViewConversionDispatcher {
 		 */
 		this.conversionApi = Object.assign( {}, conversionApi );
 
-		// `convertItem` and `convertChildren` 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 `ViewConversionDispatcher`
+		// instance and set on `conversionApi`. This way only a part of `ViewConversionDispatcher` API is exposed.
 		this.conversionApi.convertItem = this._convertItem.bind( this );
 		this.conversionApi.convertChildren = this._convertChildren.bind( this );
+		this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
 	}
 
 	/**
@@ -137,43 +159,61 @@ export default class ViewConversionDispatcher {
 	 * @fires documentFragment
 	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
 	 * Part of the view to be converted.
-	 * @param {Object} additionalData Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
-	 * events. See also {@link ~ViewConversionDispatcher#event:element element event}.
+	 * @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Elements will be converted according to this context.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process
 	 * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
 	 * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
 	 */
-	convert( viewItem, additionalData ) {
+	convert( viewItem, context = [ '$root' ] ) {
 		return this._model.change( writer => {
-			// Store writer in current conversion as a conversion API.
+			this.fire( 'viewCleanup', viewItem );
+
+			// Create context tree and set position in the top element.
+			// Items will be converted according to this position.
+			this._modelCursor = createContextTree( context, writer );
+
+			// Store writer in conversion as a conversion API
+			// to be sure that conversion process will use the same batch.
 			this.conversionApi.writer = writer;
 
-			this.fire( 'viewCleanup', viewItem );
+			// Create consumable values list for conversion process.
+			this.conversionApi.consumable = ViewConsumable.createFrom( viewItem );
 
-			const consumable = ViewConsumable.createFrom( viewItem );
-			let conversionResult = this._convertItem( viewItem, consumable, additionalData );
+			// Custom data stored by converter for conversion process.
+			this.conversionApi.store = {};
 
-			// Remove writer from conversion API after conversion.
-			this.conversionApi.writer = null;
+			// Do the conversion.
+			const { modelRange } = this._convertItem( viewItem, this._modelCursor );
 
-			// We can get a null here if conversion failed (see _convertItem())
-			// or simply if an item could not be converted (e.g. due to the schema).
-			if ( !conversionResult ) {
-				return writer.createDocumentFragment();
-			}
+			// Conversion result is always a document fragment so let's create this fragment.
+			const documentFragment = writer.createDocumentFragment();
 
-			// When conversion result is not a document fragment we need to wrap it in document fragment.
-			if ( !conversionResult.is( 'documentFragment' ) ) {
-				const docFrag = writer.createDocumentFragment();
+			// When there is a conversion result.
+			if ( modelRange ) {
+				// Remove all empty elements that was added to #_removeIfEmpty list.
+				this._removeEmptyElements();
 
-				writer.append( conversionResult, docFrag );
-				conversionResult = docFrag;
+				// Move all items that was converted to context tree to document fragment.
+				for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) {
+					writer.append( item, documentFragment );
+				}
+
+				// Extract temporary markers elements from model and set as static markers collection.
+				documentFragment.markers = extractMarkersFromModelFragment( documentFragment, writer );
 			}
 
-			// Extract temporary markers elements from model and set as static markers collection.
-			conversionResult.markers = extractMarkersFromModelFragment( conversionResult, writer );
+			// Clear context position.
+			this._modelCursor = null;
+
+			// Clear split elements.
+			this._removeIfEmpty.clear();
 
-			return conversionResult;
+			// Clear conversion API.
+			this.conversionApi.writer = null;
+			this.conversionApi.store = null;
+
+			// Return fragment as conversion result.
+			return documentFragment;
 		} );
 	}
 
@@ -181,55 +221,117 @@ export default class ViewConversionDispatcher {
 	 * @private
 	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertItem
 	 */
-	_convertItem( input, consumable, additionalData = {} ) {
-		const data = Object.assign( {}, additionalData, {
-			input,
-			output: null
-		} );
+	_convertItem( viewItem, modelCursor ) {
+		const data = Object.assign( { viewItem, modelCursor, modelRange: null } );
 
-		if ( input.is( 'element' ) ) {
-			this.fire( 'element:' + input.name, data, consumable, this.conversionApi );
-		} else if ( input.is( 'text' ) ) {
-			this.fire( 'text', data, consumable, this.conversionApi );
+		if ( viewItem.is( 'element' ) ) {
+			this.fire( 'element:' + viewItem.name, data, this.conversionApi );
+		} else if ( viewItem.is( 'text' ) ) {
+			this.fire( 'text', data, this.conversionApi );
 		} else {
-			this.fire( 'documentFragment', data, consumable, this.conversionApi );
+			this.fire( 'documentFragment', data, this.conversionApi );
 		}
 
-		// Handle incorrect `data.output`.
-		if ( data.output && !( data.output instanceof ModelNode || data.output instanceof ModelDocumentFragment ) ) {
+		// Handle incorrect conversion result.
+		if ( data.modelRange && !( data.modelRange instanceof ModelRange ) ) {
 			/**
 			 * Incorrect conversion result was dropped.
 			 *
-			 * Item may be converted to either {@link module:engine/model/node~Node model node} or
-			 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
+			 * {@link module:engine/model/range~Range Model range} should be a conversion result.
 			 *
 			 * @error view-conversion-dispatcher-incorrect-result
 			 */
-			log.warn( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.', [ input, data.output ] );
-
-			return null;
+			throw new CKEditorError( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.' );
 		}
 
-		return data.output;
+		return { modelRange: data.modelRange, modelCursor: data.modelCursor };
 	}
 
 	/**
 	 * @private
 	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren
 	 */
-	_convertChildren( input, consumable, additionalData ) {
-		const writer = this.conversionApi.writer;
-		const documentFragment = writer.createDocumentFragment();
+	_convertChildren( viewItem, modelCursor ) {
+		const modelRange = new ModelRange( modelCursor );
+		let nextModelCursor = modelCursor;
+
+		for ( const viewChild of Array.from( viewItem.getChildren() ) ) {
+			const result = this._convertItem( viewChild, nextModelCursor );
+
+			if ( result.modelRange instanceof ModelRange ) {
+				modelRange.end = result.modelRange.end;
+				nextModelCursor = result.modelCursor;
+			}
+		}
+
+		return { modelRange, modelCursor: nextModelCursor };
+	}
+
+	/**
+	 * @private
+	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#splitToAllowedParent
+	 */
+	_splitToAllowedParent( node, modelCursor ) {
+		// Try to find allowed parent.
+		const allowedParent = this.conversionApi.schema.findAllowedParent( node, modelCursor );
+
+		// When there is no parent that allows to insert node then return `null`.
+		if ( !allowedParent ) {
+			return null;
+		}
+
+		// When current position parent allows to insert node then return this position.
+		if ( allowedParent === modelCursor.parent ) {
+			return { position: modelCursor };
+		}
 
-		for ( const viewChild of Array.from( input.getChildren() ) ) {
-			const modelChild = this._convertItem( viewChild, consumable, additionalData );
+		// When allowed parent is in context tree.
+		if ( this._modelCursor.parent.getAncestors().includes( allowedParent ) ) {
+			return null;
+		}
 
-			if ( modelChild instanceof ModelNode || modelChild instanceof ModelDocumentFragment ) {
-				writer.append( modelChild, documentFragment );
+		// Split element to allowed parent.
+		const splitResult = this.conversionApi.writer.split( modelCursor, allowedParent );
+
+		// Remember all elements that are created as a result of split.
+		// This is important because at the end of conversion we want to remove all empty split elements.
+		//
+		// Loop through positions between elements in range (except split result position) and collect parents.
+		// <notSplit><split1><split2>[pos]</split2>[pos]</split1>[omit]<split1>[pos]<split2>[pos]</split2></split1></notSplit>
+		for ( const position of splitResult.range.getPositions() ) {
+			if ( !position.isEqual( splitResult.position ) ) {
+				this._removeIfEmpty.add( position.parent );
 			}
 		}
 
-		return documentFragment;
+		return {
+			position: splitResult.position,
+			cursorParent: splitResult.range.end.parent
+		};
+	}
+
+	/**
+	 * Checks if {@link #_removeIfEmpty} contains empty elements and remove them.
+	 * We need to do it smart because there could be elements that are not empty because contains
+	 * other empty elements and after removing its children they become available to remove.
+	 * We need to continue iterating over split elements as long as any element will be removed.
+	 *
+	 * @private
+	 */
+	_removeEmptyElements() {
+		let removed = false;
+
+		for ( const element of this._removeIfEmpty ) {
+			if ( element.isEmpty ) {
+				this.conversionApi.writer.remove( element );
+				this._removeIfEmpty.delete( element );
+				removed = true;
+			}
+		}
+
+		if ( removed ) {
+			this._removeEmptyElements();
+		}
 	}
 
 	/**
@@ -248,21 +350,16 @@ export default class ViewConversionDispatcher {
 	 * all elements conversion or to conversion of specific elements.
 	 *
 	 * @event element
-	 * @param {Object} data Object containing conversion input and a placeholder for conversion output and possibly other
-	 * values (see {@link #convert}).
-	 * Keep in mind that this object is shared by reference between all callbacks that will be called.
-	 * This means that callbacks can add their own values if needed,
-	 * and those values will be available in other callbacks.
-	 * @param {module:engine/view/element~Element} data.input Converted element.
-	 * @param {*} data.output The current state of conversion result. Every change to converted element should
-	 * be reflected by setting or modifying this property.
-	 * @param {module:engine/model/schema~SchemaPath} data.context The conversion context.
-	 * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ViewConversionDispatcher` constructor.
-	 * Besides of properties passed in constructor, it also has `convertItem` and `convertChildren` methods which are references
-	 * to {@link #_convertItem} and
-	 * {@link ~ViewConversionDispatcher#_convertChildren}. Those methods are needed to convert
-	 * the whole view-tree they were exposed in `conversionApi` for callbacks.
+	 * @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
+	 * 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/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.
 	 */
 
 	/**
@@ -292,16 +389,13 @@ function extractMarkersFromModelFragment( modelItem, writer ) {
 	const markers = new Map();
 
 	// Create ModelTreeWalker.
-	const walker = new ModelTreeWalker( {
-		startPosition: ModelPosition.createAt( modelItem, 0 ),
-		ignoreElementEnd: true
-	} );
+	const range = ModelRange.createIn( modelItem ).getItems();
 
 	// Walk through DocumentFragment and collect marker elements.
-	for ( const value of walker ) {
+	for ( const item of range ) {
 		// Check if current element is a marker.
-		if ( value.item.name == '$marker' ) {
-			markerElements.add( value.item );
+		if ( item.name == '$marker' ) {
+			markerElements.add( item );
 		}
 	}
 
@@ -325,36 +419,54 @@ function extractMarkersFromModelFragment( modelItem, writer ) {
 	return markers;
 }
 
+// Creates model fragment according to given context and returns position in top element.
+function createContextTree( contextDefinition, writer ) {
+	let position;
+
+	for ( const item of new SchemaContext( contextDefinition ) ) {
+		const attributes = {};
+
+		for ( const key of item.getAttributeKeys() ) {
+			attributes[ key ] = item.getAttribute( key );
+		}
+
+		const current = writer.createElement( item.name, attributes );
+
+		if ( position ) {
+			writer.append( current, position );
+		}
+
+		position = ModelPosition.createAt( current );
+	}
+
+	return position;
+}
+
 /**
  * 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}
  * fires it's events.
  *
- * `ViewConversionApi` object is built by {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher} constructor.
- * The exact list of properties of this object is determined by the object passed to the constructor.
- *
  * @interface ViewConversionApi
  */
 
 /**
  * Starts conversion of given item by firing an appropriate event.
  *
- * Every fired event is passed (as first parameter) an object with `output` property. Every event may set and/or
- * modify that property. When all callbacks are done, the final value of `output` property is returned by this method.
- * The `output` must be either {@link module:engine/model/node~Node model node} or
- * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} or `null` (as set by default).
+ * Every fired event is passed (as first parameter) an object with `modelRange` property. Every event may set and/or
+ * modify that property. When all callbacks are done, the final value of `modelRange` property is returned by this method.
+ * 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
- * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element|module:engine/view/text~Text}
- * input Item to convert.
- * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
- * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
- * events. See also {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element element event}.
- * @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment|null} The result of item conversion,
+ * @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.
+ * @returns {module:engine/model/range~Range|null} result.modelRange Model range containing result of item conversion,
  * created and modified by callbacks attached to fired event, or `null` if the conversion result was incorrect.
+ * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
  */
 
 /**
@@ -364,11 +476,37 @@ function extractMarkersFromModelFragment( modelItem, writer ) {
  * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
  * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text
  * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment
- * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
- * input Item which children will be converted.
- * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
- * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
- * events. See also {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element element event}.
- * @returns {module:engine/model/documentfragment~DocumentFragment} Model document fragment containing results of conversion
- * of all children of given item.
+ * @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.
+ * @returns {module:engine/model/range~Range} result.modelRange Model range containing results of conversion of all children of given item.
+ * When no children was converted then range is collapsed.
+ * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
+ */
+
+/**
+ * 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.
+ *
+ * @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 {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.
+ */
+
+/**
+ * Instance of {@link module:engine/conversion/viewconsumable~ViewConsumable}. It stores
+ * information about what parts of processed view item are still waiting to be handled. After a piece of view item
+ * was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
+ *
+ * @param {Object} #consumable
+ */
+
+/**
+ * Custom data stored by converter for conversion process.
+ *
+ * @param {Object} #store
  */

+ 3 - 3
packages/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js

@@ -10,14 +10,14 @@
 /* globals document */
 
 /**
- * Basic HTML writer, it uses the native `innerHTML` property for basic conversion
- * from DocumentFragment to an HTML string.
+ * Basic HTML writer. It uses the native `innerHTML` property for basic conversion
+ * from a document fragment to an HTML string.
  *
  * @implements module:engine/dataprocessor/htmlwriter~HtmlWriter
  */
 export default class BasicHtmlWriter {
 	/**
-	 * Returns HTML string created from DocumentFragment.
+	 * Returns an HTML string created from the document fragment.
 	 *
 	 * @param {DocumentFragment} fragment
 	 * @returns {String}

+ 7 - 7
packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc

@@ -8,25 +8,25 @@
  */
 
 /**
- * DataProcessor interface. It should be implemented by actual DataProcessors.
- * Each data processor implements a certain format of the data. E.g. `MarkdownDataProcessor` will convert the data
- * (Markdown string) to a {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} and back.
+ * The data processor interface. It should be implemented by actual data processors.
+ * Each data processor implements a certain format of the data. For example, Markdown data processor will convert the data
+ * (a Markdown string) to a {@link module:engine/view/documentfragment~DocumentFragment document fragment} and back.
  *
  * @interface DataProcessor
  */
 
 /**
- * Converts a {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} to data.
+ * Converts a {@link module:engine/view/documentfragment~DocumentFragment document fragment} to data.
  *
  * @method #toData
- * @param {module:engine/view/documentfragment~DocumentFragment} fragment DocumentFragment to be processed.
+ * @param {module:engine/view/documentfragment~DocumentFragment} fragment The document fragment to be processed.
  * @returns {*}
  */
 
 /**
- * Converts data to a {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment}.
+ * Converts the data to a {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
  *
  * @method #toView
- * @param {*} data Data to be processed.
+ * @param {*} data The data to be processed.
  * @returns {module:engine/view/documentfragment~DocumentFragment}
  */

+ 13 - 13
packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js

@@ -14,18 +14,18 @@ import DomConverter from '../view/domconverter';
 import { NBSP_FILLER } from '../view/filler';
 
 /**
- * HtmlDataProcessor class.
- * This data processor implementation uses HTML as input/output data.
+ * The HTML data processor class.
+ * This data processor implementation uses HTML as input and output data.
  *
  * @implements module:engine/dataprocessor/dataprocessor~DataProcessor
  */
 export default class HtmlDataProcessor {
 	/**
-	 * Creates a new instance of the HtmlDataProcessor class.
+	 * Creates a new instance of the HTML data processor class.
 	 */
 	constructor() {
 		/**
-		 * DOMParser instance used to parse HTML string to HTMLDocument.
+		 * A DOM parser instance used to parse an HTML string to an HTML document.
 		 *
 		 * @private
 		 * @member {DOMParser}
@@ -33,7 +33,7 @@ export default class HtmlDataProcessor {
 		this._domParser = new DOMParser();
 
 		/**
-		 * DOM converter used to convert DOM elements to view elements.
+		 * A DOM converter used to convert DOM elements to view elements.
 		 *
 		 * @private
 		 * @member
@@ -41,7 +41,7 @@ export default class HtmlDataProcessor {
 		this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
 
 		/**
-		 * BasicHtmlWriter instance used to convert DOM elements to HTML string.
+		 * A basic HTML writer instance used to convert DOM elements to an HTML string.
 		 *
 		 * @private
 		 * @member {module:engine/dataprocessor/basichtmlwriter~BasicHtmlWriter}
@@ -50,8 +50,8 @@ export default class HtmlDataProcessor {
 	}
 
 	/**
-	 * Converts provided {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment}
-	 * to data format - in this case HTML string.
+	 * Converts a provided {@link module:engine/view/documentfragment~DocumentFragment document fragment}
+	 * to data format &mdash; in this case to an HTML string.
 	 *
 	 * @param {module:engine/view/documentfragment~DocumentFragment} viewFragment
 	 * @returns {String} HTML string.
@@ -65,10 +65,10 @@ export default class HtmlDataProcessor {
 	}
 
 	/**
-	 * Converts provided HTML string to view tree.
+	 * Converts the provided HTML string to a view tree.
 	 *
-	 * @param {String} data HTML string.
-	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} Converted view element.
+	 * @param {String} data An HTML string.
+	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} A converted view element.
 	 */
 	toView( data ) {
 		// Convert input HTML data to DOM DocumentFragment.
@@ -79,8 +79,8 @@ export default class HtmlDataProcessor {
 	}
 
 	/**
-	 * Converts HTML String to its DOM representation. Returns DocumentFragment, containing nodes parsed from
-	 * provided data.
+	 * Converts an HTML string to its DOM representation. Returns a document fragment containing nodes parsed from
+	 * the provided data.
 	 *
 	 * @private
 	 * @param {String} data

+ 2 - 2
packages/ckeditor5-engine/src/dataprocessor/htmlwriter.js

@@ -8,13 +8,13 @@
  */
 
 /**
- * HTML writer interface.
+ * The HTML writer interface.
  *
  * @interface module:engine/dataprocessor/htmlwriter~HtmlWriter
  */
 
 /**
- * Returns HTML string created from DocumentFragment.
+ * Returns an HTML string created from a document fragment.
  *
  * @method module:engine/dataprocessor/htmlwriter~HtmlWriter#getHtml
  * @param {DocumentFragment} fragment

+ 19 - 19
packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js

@@ -14,26 +14,26 @@ import DomConverter from '../view/domconverter';
 import { NBSP_FILLER } from '../view/filler';
 
 /**
- * XmlDataProcessor class.
- * This data processor implementation uses XML as input/output data.
+ * The XML data processor class.
+ * This data processor implementation uses XML as input and output data.
  * This class is needed because unlike HTML, XML allows to use any tag with any value.
- * E.g. `<link>Text</link>` is a valid XML but invalid HTML.
+ * For example, `<link>Text</link>` is a valid XML but invalid HTML.
  *
  * @implements module:engine/dataprocessor/dataprocessor~DataProcessor
  */
 export default class XmlDataProcessor {
 	/**
-	 * Creates a new instance of the XmlDataProcessor class.
+	 * Creates a new instance of the XML data processor class.
 	 *
 	 * @param {Object} options Configuration options.
-	 * @param {Array<String>} [options.namespaces=[]] List of namespaces allowed to use in XML input.
+	 * @param {Array<String>} [options.namespaces=[]] A list of namespaces allowed to use in the XML input.
 	 */
 	constructor( options = {} ) {
 		/**
-		 * List of namespaces allowed to use in XML input.
+		 * A list of namespaces allowed to use in the XML input.
 		 *
-		 * E.g. Registering namespaces [ 'attribute', 'container' ] allows to use `<attirbute:tagName></attribute:tagName>` and
-		 * `<container:tagName></container:tagName>` input. It is mainly for debugging.
+		 * For example, registering namespaces [ 'attribute', 'container' ] allows to use `<attirbute:tagName></attribute:tagName>`
+		 * and `<container:tagName></container:tagName>` input. It is mainly for debugging.
 		 *
 		 * @public
 		 * @member {DOMParser}
@@ -41,7 +41,7 @@ export default class XmlDataProcessor {
 		this.namespaces = options.namespaces || [];
 
 		/**
-		 * DOMParser instance used to parse XML string to XMLDocument.
+		 * DOM parser instance used to parse an XML string to an XML document.
 		 *
 		 * @private
 		 * @member {DOMParser}
@@ -57,8 +57,8 @@ export default class XmlDataProcessor {
 		this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
 
 		/**
-		 * BasicHtmlWriter instance used to convert DOM elements to XML string.
-		 * There is no need to use dedicated for XML writer because BasicHtmlWriter works well in this case.
+		 * A basic HTML writer instance used to convert DOM elements to an XML string.
+		 * There is no need to use a dedicated XML writer because the basic HTML writer works well in this case.
 		 *
 		 * @private
 		 * @member {module:engine/dataprocessor/basichtmlwriter~BasicHtmlWriter}
@@ -67,11 +67,11 @@ export default class XmlDataProcessor {
 	}
 
 	/**
-	 * Converts provided {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment}
-	 * to data format- in this case XML string.
+	 * Converts the provided {@link module:engine/view/documentfragment~DocumentFragment document fragment}
+	 * to data format &mdash; in this case an XML string.
 	 *
 	 * @param {module:engine/view/documentfragment~DocumentFragment} viewFragment
-	 * @returns {String} XML string.
+	 * @returns {String} An XML string.
 	 */
 	toData( viewFragment ) {
 		// Convert view DocumentFragment to DOM DocumentFragment.
@@ -83,10 +83,10 @@ export default class XmlDataProcessor {
 	}
 
 	/**
-	 * Converts provided XML string to view tree.
+	 * Converts the provided XML string to a view tree.
 	 *
-	 * @param {String} data XML string.
-	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} Converted view element.
+	 * @param {String} data An XML string.
+	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} A converted view element.
 	 */
 	toView( data ) {
 		// Convert input XML data to DOM DocumentFragment.
@@ -97,8 +97,8 @@ export default class XmlDataProcessor {
 	}
 
 	/**
-	 * Converts XML String to its DOM representation. Returns DocumentFragment, containing nodes parsed from
-	 * provided data.
+	 * Converts an XML string to its DOM representation. Returns a document fragment containing nodes parsed from
+	 * the provided data.
 	 *
 	 * @private
 	 * @param {String} data

+ 35 - 22
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -57,7 +57,7 @@ export function getData( model, options = {} ) {
 		throw new TypeError( 'Model needs to be an instance of module:engine/model/model~Model.' );
 	}
 
-	const withoutSelection = !!options.withoutSelection;
+	const withoutSelection = options.withoutSelection;
 	const rootName = options.rootName || 'main';
 	const root = model.document.getRoot( rootName );
 
@@ -277,8 +277,12 @@ export function parse( data, schema, options = {} ) {
 	viewToModel.on( 'element', convertToModelElement() );
 	viewToModel.on( 'text', convertToModelText() );
 
+	viewToModel.isDebug = true;
+
 	// Convert view to model.
-	let model = viewToModel.convert( viewDocumentFragment.root, { context: options.context || [ '$root' ] } );
+	let model = viewToModel.convert( viewDocumentFragment.root, options.context || [ '$root' ] );
+
+	mapper.bindElements( model, viewDocumentFragment.root );
 
 	// If root DocumentFragment contains only one element - return that element.
 	if ( model.childCount == 1 ) {
@@ -316,41 +320,47 @@ export function parse( data, schema, options = {} ) {
 // -- Converters view -> model -----------------------------------------------------
 
 function convertToModelFragment() {
-	return ( evt, data, consumable, conversionApi ) => {
-		data.output = conversionApi.convertChildren( data.input, consumable, data );
-		conversionApi.mapper.bindElements( data.output, data.input );
+	return ( evt, data, conversionApi ) => {
+		const childrenResult = conversionApi.convertChildren( data.viewItem, data.modelCursor );
+
+		conversionApi.mapper.bindElements( data.modelCursor.parent, data.viewItem );
+
+		data = Object.assign( data, childrenResult );
 
 		evt.stop();
 	};
 }
 
 function convertToModelElement() {
-	return ( evt, data, consumable, conversionApi ) => {
-		const elementName = data.input.name;
+	return ( evt, data, conversionApi ) => {
+		const elementName = data.viewItem.name;
 
-		if ( !conversionApi.schema.checkChild( data.context, elementName ) ) {
-			throw new Error( `Element '${ elementName }' was not allowed in context ${ JSON.stringify( data.context ) }.` );
+		if ( !conversionApi.schema.checkChild( data.modelCursor, elementName ) ) {
+			throw new Error( `Element '${ elementName }' was not allowed in given position.` );
 		}
 
 		// View attribute value is a string so we want to typecast it to the original type.
 		// E.g. `bold="true"` - value will be parsed from string `"true"` to boolean `true`.
-		const attributes = convertAttributes( data.input.getAttributes(), parseAttributeValue );
+		const attributes = convertAttributes( data.viewItem.getAttributes(), parseAttributeValue );
+		const element = conversionApi.writer.createElement( data.viewItem.name, attributes );
 
-		data.output = conversionApi.writer.createElement( data.input.name, attributes );
-		conversionApi.mapper.bindElements( data.output, data.input );
+		conversionApi.writer.insert( element, data.modelCursor );
 
-		data.context.push( data.output );
-		data.output.appendChildren( conversionApi.convertChildren( data.input, consumable, data ) );
-		data.context.pop();
+		conversionApi.mapper.bindElements( element, data.viewItem );
+
+		conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( element ) );
+
+		data.modelRange = ModelRange.createOn( element );
+		data.modelCursor = data.modelRange.end;
 
 		evt.stop();
 	};
 }
 
 function convertToModelText( withAttributes = false ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( !conversionApi.schema.checkChild( data.context, '$text' ) ) {
-			throw new Error( `Text was not allowed in context ${ JSON.stringify( data.context ) }.` );
+	return ( evt, data, conversionApi ) => {
+		if ( !conversionApi.schema.checkChild( data.modelCursor, '$text' ) ) {
+			throw new Error( 'Text was not allowed in given position.' );
 		}
 
 		let node;
@@ -358,14 +368,17 @@ function convertToModelText( withAttributes = false ) {
 		if ( withAttributes ) {
 			// View attribute value is a string so we want to typecast it to the original type.
 			// E.g. `bold="true"` - value will be parsed from string `"true"` to boolean `true`.
-			const attributes = convertAttributes( data.input.getAttributes(), parseAttributeValue );
+			const attributes = convertAttributes( data.viewItem.getAttributes(), parseAttributeValue );
 
-			node = conversionApi.writer.createText( data.input.getChild( 0 ).data, attributes );
+			node = conversionApi.writer.createText( data.viewItem.getChild( 0 ).data, attributes );
 		} else {
-			node = conversionApi.writer.createText( data.input.data );
+			node = conversionApi.writer.createText( data.viewItem.data );
 		}
 
-		data.output = node;
+		conversionApi.writer.insert( node, data.modelCursor );
+
+		data.modelRange = ModelRange.createFromPositionAndShift( data.modelCursor, node.offsetSize );
+		data.modelCursor = data.modelRange.end;
 
 		evt.stop();
 	};

+ 3 - 3
packages/ckeditor5-engine/src/model/differ.js

@@ -786,10 +786,10 @@ export default class Differ {
 					attributeNewValue: newValue,
 					changeCount: this._changeCount++
 				} );
-
-				// Prevent returning two diff items for the same change.
-				newAttributes.delete( key );
 			}
+
+			// Prevent returning two diff items for the same change.
+			newAttributes.delete( key );
 		}
 
 		// Look through new attributes that weren't handled above.

+ 59 - 13
packages/ckeditor5-engine/src/model/schema.js

@@ -376,8 +376,9 @@ export default class Schema {
 	 *		schema.checkChild( model.document.getRoot(), paragraph ); // -> true
 	 *
 	 * @fires checkChild
-	 * @param {module:engine/model/schema~SchemaContextDefinition} context Context in which the child will be checked.
-	 * @param {module:engine/model/node~Node|String} child The child to check.
+	 * @param {module:engine/model/schema~SchemaContextDefinition|module:engine/model/schema~SchemaContext} context
+	 * Context in which the child will be checked.
+	 * @param {module:engine/model/node~Node|String} def The child to check.
 	 */
 	checkChild( context, def ) {
 		// Note: context and child are already normalized here to a SchemaContext and SchemaCompiledItemDefinition.
@@ -400,7 +401,8 @@ export default class Schema {
 	 *		schema.checkAttribute( textNode, 'bold' ); // -> true
 	 *
 	 * @fires checkAttribute
-	 * @param {module:engine/model/schema~SchemaContextDefinition} context
+	 * @param {module:engine/model/schema~SchemaContextDefinition|module:engine/model/schema~SchemaContext} context
+	 * Context in which the attribute will be checked.
 	 * @param {String} attributeName
 	 */
 	checkAttribute( context, attributeName ) {
@@ -687,7 +689,6 @@ export default class Schema {
 	 * @param {'both'|'forward'|'backward'} [direction='both'] Search direction.
 	 * @returns {module:engine/model/range~Range|null} Nearest selection range or `null` if one cannot be found.
 	 */
-
 	getNearestSelectionRange( position, direction = 'both' ) {
 		// Return collapsed range if provided position is valid.
 		if ( this.checkChild( position, '$text' ) ) {
@@ -720,6 +721,33 @@ export default class Schema {
 		return null;
 	}
 
+	/**
+	 * Tries to find position ancestors that allows to insert given node.
+	 * It starts searching from the given position and goes node by node to the top of the model tree
+	 * as long as {@link module:engine/model/schema~Schema#isLimit limit element} or top-most ancestor won't be reached.
+	 *
+	 * @params {module:engine/model/node~Node} node Node for which allowed parent should be found.
+	 * @params {module:engine/model/position~Position} position Position from searching will start.
+	 * @returns {module:engine/model/element~Element|null} element Allowed parent or null if nothing was found.
+	 */
+	findAllowedParent( node, position ) {
+		let parent = position.parent;
+
+		while ( parent ) {
+			if ( this.checkChild( parent, node ) ) {
+				return parent;
+			}
+
+			if ( this.isLimit( parent ) ) {
+				return null;
+			}
+
+			parent = parent.parent;
+		}
+
+		return null;
+	}
+
 	/**
 	 * Removes attributes disallowed by the schema.
 	 *
@@ -1086,22 +1114,21 @@ export class SchemaContext {
 	/**
 	 * Creates an instance of the context.
 	 *
-	 * @param {module:engine/model/schema~SchemaContextDefinition} context
+	 * @param {module:engine/model/schema~SchemaContextDefinition|module:engine/model/schema~SchemaContext} context
 	 */
 	constructor( context ) {
-		if ( Array.isArray( context ) ) {
-			if ( context[ 0 ] && typeof context[ 0 ] != 'string' && context[ 0 ].is( 'documentFragment' ) ) {
-				context.shift();
-			}
+		if ( context instanceof SchemaContext ) {
+			return context;
 		}
-		else {
+
+		if ( !Array.isArray( context ) ) {
 			// `context` is item or position.
 			// Position#getAncestors() doesn't accept any parameters but it works just fine here.
 			context = context.getAncestors( { includeSelf: true } );
+		}
 
-			if ( context[ 0 ].is( 'documentFragment' ) ) {
-				context.shift();
-			}
+		if ( context[ 0 ] && typeof context[ 0 ] != 'string' && context[ 0 ].is( 'documentFragment' ) ) {
+			context.shift();
 		}
 
 		this._items = context.map( mapContextItem );
@@ -1136,6 +1163,25 @@ export class SchemaContext {
 		return this._items[ Symbol.iterator ]();
 	}
 
+	/**
+	 * Returns new SchemaContext instance with additional items created from provided definition.
+	 *
+	 * @param {String|module:engine/model/node~Node|module:engine/model/schema~SchemaContext|
+	 * Array<String|module:engine/model/node~Node>} definition Definition of item(s) that will be added to current context.
+	 * @returns {module:engine/model/schema~SchemaContext} New SchemaContext instance.
+	 */
+	concat( definition ) {
+		if ( !( definition instanceof SchemaContext ) && !Array.isArray( definition ) ) {
+			definition = [ definition ];
+		}
+
+		const ctx = new SchemaContext( definition );
+
+		ctx._items = [ ...this._items, ...ctx._items ];
+
+		return ctx;
+	}
+
 	/**
 	 * Gets an item on the given index.
 	 *

+ 44 - 2
packages/ckeditor5-engine/src/model/utils/insertcontent.js

@@ -165,6 +165,7 @@ class Insertion {
 	}
 
 	/**
+	 * @private
 	 * Handles insertion of a single node.
 	 *
 	 * @param {module:engine/model/node~Node} node
@@ -211,6 +212,7 @@ class Insertion {
 	}
 
 	/**
+	 * @private
 	 * @param {module:engine/model/element~Element} node The object element.
 	 * @param {Object} context
 	 */
@@ -226,6 +228,7 @@ class Insertion {
 	}
 
 	/**
+	 * @private
 	 * @param {module:engine/model/node~Node} node The disallowed node which needs to be handled.
 	 * @param {Object} context
 	 */
@@ -241,6 +244,7 @@ class Insertion {
 	}
 
 	/**
+	 * @private
 	 * @param {module:engine/model/node~Node} node The node to insert.
 	 */
 	_insert( node ) {
@@ -274,6 +278,7 @@ class Insertion {
 	}
 
 	/**
+	 * @private
 	 * @param {module:engine/model/node~Node} node The node which could potentially be merged.
 	 * @param {Object} context
 	 */
@@ -282,8 +287,8 @@ class Insertion {
 			return;
 		}
 
-		const mergeLeft = context.isFirst && ( node.previousSibling instanceof Element ) && this.canMergeWith.has( node.previousSibling );
-		const mergeRight = context.isLast && ( node.nextSibling instanceof Element ) && this.canMergeWith.has( node.nextSibling );
+		const mergeLeft = this._canMergeLeft( node, context );
+		const mergeRight = this._canMergeRight( node, context );
 		const mergePosLeft = LivePosition.createBefore( node );
 		const mergePosRight = LivePosition.createAfter( node );
 
@@ -329,9 +334,44 @@ class Insertion {
 		mergePosRight.detach();
 	}
 
+	/**
+	 * Checks whether specified node can be merged with previous sibling element.
+	 *
+	 * @private
+	 * @param {module:engine/model/node~Node} node The node which could potentially be merged.
+	 * @param {Object} context
+	 * @returns {Boolean}
+	 */
+	_canMergeLeft( node, context ) {
+		const previousSibling = node.previousSibling;
+
+		return context.isFirst &&
+			( previousSibling instanceof Element ) &&
+			this.canMergeWith.has( previousSibling ) &&
+			this.model.schema.checkMerge( previousSibling, node );
+	}
+
+	/**
+	 * Checks whether specified node can be merged with next sibling element.
+	 *
+	 * @private
+	 * @param {module:engine/model/node~Node} node The node which could potentially be merged.
+	 * @param {Object} context
+	 * @returns {Boolean}
+	 */
+	_canMergeRight( node, context ) {
+		const nextSibling = node.nextSibling;
+
+		return context.isLast &&
+			( nextSibling instanceof Element ) &&
+			this.canMergeWith.has( nextSibling ) &&
+			this.model.schema.checkMerge( node, nextSibling );
+	}
+
 	/**
 	 * Tries wrapping the node in a new paragraph and inserting it this way.
 	 *
+	 * @private
 	 * @param {module:engine/model/node~Node} node The node which needs to be autoparagraphed.
 	 * @param {Object} context
 	 */
@@ -348,6 +388,7 @@ class Insertion {
 	}
 
 	/**
+	 * @private
 	 * @param {module:engine/model/node~Node} node
 	 * @returns {Boolean} Whether an allowed position was found.
 	 * `false` is returned if the node isn't allowed at any position up in the tree, `true` if was.
@@ -393,6 +434,7 @@ class Insertion {
 	/**
 	 * Gets the element in which the given node is allowed. It checks the passed element and all its ancestors.
 	 *
+	 * @private
 	 * @param {module:engine/model/node~Node} node The node to check.
 	 * @param {module:engine/model/element~Element} element The element in which the node's correctness should be checked.
 	 * @returns {module:engine/model/element~Element|null}

+ 61 - 25
packages/ckeditor5-engine/src/model/writer.js

@@ -573,20 +573,23 @@ export default class Writer {
 	}
 
 	/**
-	 * Splits an element at the given position.
+	 * Splits elements start from the given position and goes to the top of the model tree as long as given
+	 * `limitElement` won't be reached. When limitElement is not defined then only a parent of given position will be split.
 	 *
 	 * The element needs to have a parent. It cannot be a root element nor document fragment.
 	 * The `writer-split-element-no-parent` error will be thrown if you try to split an element with no parent.
 	 *
 	 * @param {module:engine/model/position~Position} position Position of split.
+	 * @param {module:engine/model/node~Node} [limitElement] Stop splitting when this element will be reached.
+	 * @returns {Object} result Split result.
+	 * @returns {module:engine/model/position~Position} result.position between split elements.
+	 * @returns {module:engine/model/range~Range} result.range Range that stars from the end of the first split element and ands
+	 * at the beginning of the first copy element.
 	 */
-	split( position ) {
+	split( position, limitElement ) {
 		this._assertWriterUsedCorrectly();
 
-		const delta = new SplitDelta();
-		this.batch.addDelta( delta );
-
-		const splitElement = position.parent;
+		let splitElement = position.parent;
 
 		if ( !splitElement.parent ) {
 			/**
@@ -597,30 +600,63 @@ export default class Writer {
 			throw new CKEditorError( 'writer-split-element-no-parent: Element with no parent can not be split.' );
 		}
 
-		const copy = new Element( splitElement.name, splitElement.getAttributes() );
-		const insertVersion = splitElement.root.document ? splitElement.root.document.version : null;
+		// When limit element is not defined lets set splitElement parent as limit.
+		if ( !limitElement ) {
+			limitElement = splitElement.parent;
+		}
 
-		const insert = new InsertOperation(
-			Position.createAfter( splitElement ),
-			copy,
-			insertVersion
-		);
+		if ( !position.parent.getAncestors( { includeSelf: true } ).includes( limitElement ) ) {
+			throw new CKEditorError( 'writer-split-invalid-limit-element: Limit element is not a position ancestor.' );
+		}
 
-		delta.addOperation( insert );
-		this.model.applyOperation( insert );
+		// We need to cache elements that will be created as a result of the first split because
+		// we need to create a range from the end of the first split element to the beginning of the
+		// first copy element. This should be handled by LiveRange but it doesn't work on detached nodes.
+		let firstSplitElement, firstCopyElement;
 
-		const moveVersion = insertVersion !== null ? insertVersion + 1 : null;
+		do {
+			const delta = new SplitDelta();
+			this.batch.addDelta( delta );
 
-		const move = new MoveOperation(
-			position,
-			splitElement.maxOffset - position.offset,
-			Position.createFromParentAndOffset( copy, 0 ),
-			moveVersion
-		);
-		move.isSticky = true;
+			const copy = new Element( splitElement.name, splitElement.getAttributes() );
+			const insertVersion = splitElement.root.document ? splitElement.root.document.version : null;
 
-		delta.addOperation( move );
-		this.model.applyOperation( move );
+			const insert = new InsertOperation(
+				Position.createAfter( splitElement ),
+				copy,
+				insertVersion
+			);
+
+			delta.addOperation( insert );
+			this.model.applyOperation( insert );
+
+			const moveVersion = insertVersion !== null ? insertVersion + 1 : null;
+
+			const move = new MoveOperation(
+				position,
+				splitElement.maxOffset - position.offset,
+				Position.createFromParentAndOffset( copy, 0 ),
+				moveVersion
+			);
+			move.isSticky = true;
+
+			delta.addOperation( move );
+			this.model.applyOperation( move );
+
+			// Cache result of the first split.
+			if ( !firstSplitElement && !firstCopyElement ) {
+				firstSplitElement = splitElement;
+				firstCopyElement = copy;
+			}
+
+			position = Position.createBefore( copy );
+			splitElement = position.parent;
+		} while ( splitElement !== limitElement );
+
+		return {
+			position,
+			range: new Range( Position.createAt( firstSplitElement, 'end' ), Position.createAt( firstCopyElement ) )
+		};
 	}
 
 	/**

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

@@ -97,7 +97,7 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should accept parsing context', () => {
-			const output = data.parse( 'foo', '$block' );
+			const output = data.parse( 'foo', [ '$block' ] );
 
 			expect( stringify( output ) ).to.equal( 'foo' );
 		} );
@@ -138,7 +138,7 @@ describe( 'DataController', () => {
 			expect( stringify( data.toModel( viewFragment ) ) ).to.equal( '' );
 
 			// Model fragment in inline root.
-			expect( stringify( data.toModel( viewFragment, 'inlineRoot' ) ) ).to.equal( 'foo' );
+			expect( stringify( data.toModel( viewFragment, [ 'inlineRoot' ] ) ) ).to.equal( 'foo' );
 		} );
 	} );
 

+ 0 - 553
packages/ckeditor5-engine/tests/conversion/advanced-converters.js

@@ -1,553 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../src/model/model';
-import ModelElement from '../../src/model/element';
-import ModelText from '../../src/model/text';
-import ModelTextProxy from '../../src/model/textproxy';
-import ModelRange from '../../src/model/range';
-import ModelPosition from '../../src/model/position';
-import ModelWalker from '../../src/model/treewalker';
-
-import ViewElement from '../../src/view/element';
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewAttributeElement from '../../src/view/attributeelement';
-import ViewText from '../../src/view/text';
-import viewWriter from '../../src/view/writer';
-import ViewPosition from '../../src/view/position';
-import ViewRange from '../../src/view/range';
-
-import EditingController from '../../src/controller/editingcontroller';
-
-import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
-
-import {
-	insertElement,
-	changeAttribute,
-	wrap
-} from '../../src/conversion/model-to-view-converters';
-import { convertToModelFragment, convertText } from '../../src/conversion/view-to-model-converters';
-
-describe( 'advanced-converters', () => {
-	let model, modelDoc, modelRoot, viewRoot, modelDispatcher, viewDispatcher;
-
-	beforeEach( () => {
-		model = new Model();
-		modelDoc = model.document;
-		modelRoot = modelDoc.createRoot();
-
-		const editing = new EditingController( model );
-
-		viewRoot = editing.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';
-
-		viewDispatcher = new ViewConversionDispatcher( model, { schema: { checkChild: () => true } } );
-		viewDispatcher.on( 'text', convertText() );
-		viewDispatcher.on( 'documentFragment', convertToModelFragment() );
-
-		modelDispatcher = editing.modelToView;
-	} );
-
-	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;
-	}
-
-	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;
-	}
-
-	// Converter overwrites default attribute converter for `linkHref` and `linkTitle` attribute is set on `quote` element.
-	//
-	// Model:
-	//
-	// [quote {linkHref='foo.html' linkTitle='Foo source'}]
-	//   ├─ f
-	//   ├─ o
-	//   └─ o
-	//
-	// foo {linkHref='foo.html' linkTitle='Foo title'}
-	//
-	// View:
-	//
-	// <blockquote>
-	//	 ├─ foo
-	//	 └─ <a href="foo.html" title="Foo source">
-	//	 	  └─ see source
-	//
-	// <a href="foo.html" title="Foo title">
-	//	 └─ foo
-	describe( 'custom attribute handling for given element', () => {
-		beforeEach( () => {
-			// Normal model-to-view converters for links.
-			modelDispatcher.on( 'attribute:linkHref', wrap( value => value ? new ViewAttributeElement( 'a', { href: value } ) : null ) );
-			modelDispatcher.on( 'attribute:linkTitle', wrap( value => value ? new ViewAttributeElement( 'a', { title: value } ) : null ) );
-
-			// Normal view-to-model converters for links.
-			viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => {
-				if ( consumable.consume( data.input, { name: true, attribute: 'href' } ) ) {
-					if ( !data.output ) {
-						data.output = conversionApi.convertChildren( data.input, consumable );
-					}
-
-					for ( const child of data.output ) {
-						child.setAttribute( 'linkHref', data.input.getAttribute( 'href' ) );
-					}
-				}
-			} );
-
-			viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => {
-				if ( consumable.consume( data.input, { attribute: 'title' } ) ) {
-					if ( !data.output ) {
-						data.output = conversionApi.convertChildren( data.input, consumable );
-					}
-
-					for ( const child of data.output ) {
-						child.setAttribute( 'linkTitle', data.input.getAttribute( 'title' ) );
-					}
-				}
-			} );
-
-			// Model-to-view converter for quote element.
-			modelDispatcher.on( 'insert:quote', ( evt, data, consumable, conversionApi ) => {
-				consumable.consume( data.item, 'insert' );
-
-				const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
-				const viewElement = new ViewContainerElement( 'blockquote' );
-
-				conversionApi.mapper.bindElements( data.item, viewElement );
-				viewWriter.insert( viewPosition, viewElement );
-			}, { priority: 'high' } );
-
-			modelDispatcher.on( 'attribute:linkHref:quote', linkHrefOnQuoteConverter, { priority: 'high' } );
-			modelDispatcher.on( 'attribute:linkTitle:quote', linkTitleOnQuoteConverter, { priority: 'high' } );
-
-			function linkHrefOnQuoteConverter( evt, data, consumable, conversionApi ) {
-				if ( !consumable.consume( data.item, 'attribute:linkHref' ) ) {
-					return;
-				}
-
-				const viewQuote = conversionApi.mapper.toViewElement( data.item );
-
-				if ( data.attributeNewValue === null ) {
-					// Attribute was removed -> remove the view link.
-					const viewLink = viewQuote.getChild( viewQuote.childCount - 1 );
-
-					viewWriter.remove( ViewRange.createOn( viewLink ) );
-
-					consumable.consume( data.item, 'attribute:linkTitle' );
-				} else if ( data.attributeOldValue === null ) {
-					// Attribute was added -> add the view link.
-					const viewLink = new ViewAttributeElement(
-						'a', { href: data.item.getAttribute( 'linkHref' ) }, new ViewText( 'see source' )
-					);
-
-					if ( consumable.consume( data.item, 'attribute:linkTitle' ) && data.item.getAttribute( 'linkTitle' ) !== null ) {
-						viewLink.setAttribute( 'title', data.item.getAttribute( 'linkTitle' ) );
-					}
-
-					viewWriter.insert( new ViewPosition( viewQuote, viewQuote.childCount ), viewLink );
-				} else {
-					// Attribute has changed -> change the existing view link.
-					const viewLink = viewQuote.getChild( viewQuote.childCount - 1 );
-					viewLink.setAttribute( 'href', data.attributeNewValue );
-				}
-			}
-
-			function linkTitleOnQuoteConverter( evt, data, consumable, conversionApi ) {
-				if ( !consumable.consume( data.item, 'attribute:linkTitle' ) ) {
-					return;
-				}
-
-				const viewQuote = conversionApi.mapper.toViewElement( data.item );
-				const viewLink = viewQuote.getChild( viewQuote.childCount - 1 );
-
-				if ( !viewLink ) {
-					return;
-				}
-
-				if ( data.attributeNewValue === null ) {
-					viewLink.removeAttribute( 'title' );
-				} else {
-					viewLink.setAttribute( 'title', data.attributeNewValue );
-				}
-			}
-
-			// View-to-model converter for quote element.
-			viewDispatcher.on( 'element:blockquote', ( evt, data, consumable, conversionApi ) => {
-				if ( consumable.consume( data.input, { name: true } ) ) {
-					data.output = new ModelElement( 'quote' );
-
-					const viewA = data.input.getChild( data.input.childCount - 1 );
-
-					// Convert the special "a" first, before converting all children.
-					if ( viewA instanceof ViewElement && viewA.name == 'a' && consumable.consume( viewA, { name: true } ) ) {
-						if ( consumable.consume( viewA, { attribute: 'href' } ) ) {
-							data.output.setAttribute( 'linkHref', viewA.getAttribute( 'href' ) );
-						}
-
-						if ( consumable.consume( viewA, { attribute: 'title' } ) ) {
-							data.output.setAttribute( 'linkTitle', viewA.getAttribute( 'title' ) );
-						}
-					}
-
-					const children = conversionApi.convertChildren( data.input, consumable );
-					data.output.appendChildren( children );
-				}
-			} );
-		} );
-
-		it( 'should convert model text with linkHref and linkTitle to view', () => {
-			const modelText = new ModelText( 'foo', { linkHref: 'foo.html', linkTitle: 'Foo title' } );
-
-			// Let's insert text with link attributes.
-			model.change( writer => {
-				writer.insert(
-					modelText,
-					new ModelPosition( modelRoot, [ 0 ] )
-				);
-			} );
-
-			let range = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><a href="foo.html" title="Foo title">foo</a></div>' );
-
-			// Let's change link's attributes.
-			model.change( writer => {
-				writer.setAttribute( 'linkHref', 'bar.html', range );
-				writer.setAttribute( 'linkTitle', 'Bar title', range );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><a href="bar.html" title="Bar title">foo</a></div>' );
-
-			// Let's remove a letter from the link.
-			model.change( writer => {
-				writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 ) );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><a href="bar.html" title="Bar title">oo</a></div>' );
-
-			// Let's remove just one attribute.
-			model.change( writer => {
-				range = ModelRange.createIn( modelRoot );
-				writer.removeAttribute( 'linkTitle', range );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div><a href="bar.html">oo</a></div>' );
-
-			// Let's remove the other attribute.
-			model.change( writer => {
-				range = ModelRange.createIn( modelRoot );
-				writer.removeAttribute( 'linkHref', range );
-			} );
-
-			expect( viewToString( viewRoot ) ).to.equal( '<div>oo</div>' );
-		} );
-
-		it( 'should convert a view element to model', () => {
-			const viewElement = new ViewAttributeElement( 'a', { href: 'foo.html', title: 'Foo title' }, new ViewText( 'foo' ) );
-
-			const modelText = viewDispatcher.convert( viewElement ).getChild( 0 );
-
-			expect( modelText ).to.be.instanceof( ModelText );
-			expect( modelText.data ).to.equal( 'foo' );
-			expect( modelText.getAttribute( 'linkHref' ) ).to.equal( 'foo.html' );
-			expect( modelText.getAttribute( 'linkTitle' ) ).to.equal( 'Foo title' );
-		} );
-
-		it( 'should convert quote model element with linkHref and linkTitle attribute to view', () => {
-			modelDispatcher.on( 'attribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
-
-			const modelElement = new ModelElement( 'quote', { linkHref: 'foo.html', linkTitle: 'Foo source' }, new ModelText( 'foo' ) );
-
-			// Let's insert a quote element with link attribute.
-			model.change( writer => {
-				writer.insert(
-					modelElement,
-					new ModelPosition( modelRoot, [ 0 ] )
-				);
-			} );
-
-			let expected = '<div><blockquote>foo<a href="foo.html" title="Foo source">see source</a></blockquote></div>';
-			expect( viewToString( viewRoot ) ).to.equal( expected );
-
-			// And insert some additional content into it.
-			model.change( writer => {
-				writer.insert(
-					new ModelText( 'bar', { bold: true } ),
-					new ModelPosition( modelRoot, [ 0, 3 ] )
-				);
-			} );
-
-			expected = '<div><blockquote>foo<strong>bar</strong><a href="foo.html" title="Foo source">see source</a></blockquote></div>';
-			expect( viewToString( viewRoot ) ).to.equal( expected );
-
-			// Let's change some attributes.
-			model.change( writer => {
-				writer.removeAttribute( 'linkTitle', modelElement );
-				writer.setAttribute( 'linkHref', 'bar.html', modelElement );
-			} );
-
-			expected = '<div><blockquote>foo<strong>bar</strong><a href="bar.html">see source</a></blockquote></div>';
-			expect( viewToString( viewRoot ) ).to.equal( expected );
-
-			// Let's remove the only attribute connected with link.
-			model.change( writer => {
-				writer.removeAttribute( 'linkHref', modelElement );
-			} );
-
-			expected = '<div><blockquote>foo<strong>bar</strong></blockquote></div>';
-			expect( viewToString( viewRoot ) ).to.equal( expected );
-		} );
-
-		it( 'should convert view blockquote with a element to model', () => {
-			const viewElement = new ViewContainerElement(
-				'blockquote',
-				null,
-				[
-					new ViewText( 'foo' ),
-					new ViewAttributeElement(
-						'a',
-						{
-							href: 'foo.html',
-							title: 'Foo source'
-						},
-						new ViewText( 'see source' )
-					)
-				]
-			);
-
-			const modelElement = viewDispatcher.convert( viewElement );
-
-			expect( modelToString( modelElement ) ).to.equal( '<quote linkHref="foo.html" linkTitle="Foo source">foo</quote>' );
-		} );
-	} );
-
-	// Default view converter for tables that will convert table structure into paragraphs if tables are not supported.
-	// TRs are supposed to become paragraphs and TDs content should be separated using space.
-	it( 'default table view to model converter', () => {
-		viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => {
-			if ( consumable.consume( data.input, { name: true, attribute: 'href' } ) ) {
-				if ( !data.output ) {
-					data.output = conversionApi.convertChildren( data.input, consumable );
-				}
-
-				for ( const child of data.output ) {
-					child.setAttribute( 'linkHref', data.input.getAttribute( 'href' ) );
-				}
-			}
-		} );
-
-		viewDispatcher.on( 'element:tr', ( evt, data, consumable, conversionApi ) => {
-			if ( consumable.consume( data.input, { name: true } ) ) {
-				data.output = new ModelElement( 'paragraph' );
-
-				const children = conversionApi.convertChildren( data.input, consumable );
-
-				for ( let i = 1; i < children.childCount; i++ ) {
-					const child = children.getChild( i );
-
-					if ( child instanceof ModelText && child.previousSibling instanceof ModelText ) {
-						children.insertChildren( i, new ModelText( ' ' ) );
-						i++;
-					}
-				}
-
-				data.output.appendChildren( children );
-			}
-		} );
-
-		viewDispatcher.on( 'element:table', ( evt, data, consumable, conversionApi ) => {
-			if ( consumable.consume( data.input, { name: true } ) ) {
-				data.output = conversionApi.convertChildren( data.input, consumable );
-			}
-		} );
-
-		viewDispatcher.on( 'element:td', ( evt, data, consumable, conversionApi ) => {
-			if ( consumable.consume( data.input, { name: true } ) ) {
-				data.output = conversionApi.convertChildren( data.input, consumable );
-			}
-		} );
-
-		const viewTable = new ViewContainerElement( 'table', null, [
-			new ViewContainerElement( 'tr', null, [
-				new ViewContainerElement( 'td', null, new ViewText( 'foo' ) ),
-				new ViewContainerElement( 'td', null, new ViewAttributeElement( 'a', { href: 'bar.html' }, new ViewText( 'bar' ) ) )
-			] ),
-			new ViewContainerElement( 'tr', null, [
-				new ViewContainerElement( 'td' ),
-				new ViewContainerElement( 'td', null, new ViewText( 'abc' ) )
-			] )
-		] );
-
-		expect( modelToString( viewDispatcher.convert( viewTable ) ) )
-			.to.equal( '<paragraph>foo <$text linkHref="bar.html">bar</$text></paragraph><paragraph>abc</paragraph>' );
-	} );
-
-	// Model converter that converts any non-converted elements and attributes into view elements and attributes.
-	// View converter that converts any non-converted elements and attributes into model elements and attributes.
-	describe( 'universal converter', () => {
-		beforeEach( () => {
-			// "Universal" converters
-			modelDispatcher.on( 'insert', insertElement( data => new ViewContainerElement( data.item.name ) ), { priority: 'lowest' } );
-			modelDispatcher.on( 'attribute', changeAttribute(), { priority: 'lowest' } );
-
-			viewDispatcher.on( 'element', ( evt, data, consumable, conversionApi ) => {
-				if ( consumable.consume( data.input, { name: true } ) ) {
-					data.output = new ModelElement( data.input.name );
-
-					for ( const key of data.input.getAttributeKeys() ) {
-						if ( consumable.consume( data.input, { attribute: key } ) ) {
-							data.output.setAttribute( key, data.input.getAttribute( key ) );
-						}
-					}
-
-					data.output.appendChildren( conversionApi.convertChildren( data.input, consumable ) );
-				}
-			}, { priority: 'lowest' } );
-
-			// "Real" converters -- added with higher priority. Should overwrite the "universal" converters.
-			modelDispatcher.on( 'insert:image', insertElement( new ViewContainerElement( 'img' ) ) );
-			modelDispatcher.on( 'attribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
-
-			viewDispatcher.on( 'element:img', ( evt, data, consumable ) => {
-				if ( consumable.consume( data.input, { name: true } ) ) {
-					const modelImage = new ModelElement( 'image' );
-
-					for ( const attributeKey of data.input.getAttributeKeys() ) {
-						modelImage.setAttribute( attributeKey, data.input.getAttribute( attributeKey ) );
-					}
-
-					data.output = modelImage;
-				}
-			} );
-			viewDispatcher.on( 'element:strong', ( evt, data, consumable, conversionApi ) => {
-				if ( consumable.consume( data.input, { name: true } ) ) {
-					if ( !data.output ) {
-						data.output = conversionApi.convertChildren( data.input, consumable );
-					}
-
-					for ( const child of data.output ) {
-						child.setAttribute( 'bold', true );
-					}
-				}
-			} );
-		} );
-
-		it( 'should convert model to view', () => {
-			const modelElement = new ModelElement( 'table', { cellpadding: 5, cellspacing: 5 }, [
-				new ModelElement( 'tr', null, [
-					new ModelElement( 'td', null, [
-						new ModelText( 'foo ' ),
-						new ModelText( 'abc', { bold: true } ),
-						new ModelText( ' bar' )
-					] ),
-					new ModelElement( 'td', null, [
-						new ModelElement( 'foo', { foo: 'bar' }, new ModelText( 'bar' ) )
-					] )
-				] )
-			] );
-
-			modelRoot.appendChildren( modelElement );
-			modelDispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
-
-			expect( viewToString( viewRoot ) ).to.equal(
-				'<div>' +
-					'<table cellpadding="5" cellspacing="5">' +
-						'<tr>' +
-							'<td>foo <strong>abc</strong> bar</td>' +
-							'<td><foo foo="bar">bar</foo></td>' +
-						'</tr>' +
-					'</table>' +
-				'</div>'
-			);
-		} );
-
-		it( 'should convert view to model', () => {
-			const viewElement = new ViewContainerElement( 'table', { cellpadding: 5, cellspacing: 5 }, [
-				new ViewContainerElement( 'tr', null, [
-					new ViewContainerElement( 'td', null, [
-						new ViewText( 'foo ' ),
-						new ViewAttributeElement( 'strong', null, new ViewText( 'abc' ) ),
-						new ViewText( ' bar' )
-					] ),
-					new ViewContainerElement( 'td', null, new ViewContainerElement( 'foo', { foo: 'bar' }, new ViewText( 'bar' ) ) )
-				] )
-			] );
-
-			const modelElement = viewDispatcher.convert( viewElement );
-
-			expect( modelToString( modelElement ) ).to.equal(
-				'<table cellpadding="5" cellspacing="5">' +
-					'<tr>' +
-						'<td>foo <$text bold="true">abc</$text> bar</td>' +
-						'<td><foo foo="bar">bar</foo></td>' +
-					'</tr>' +
-				'</table>'
-			);
-		} );
-	} );
-} );

+ 95 - 61
packages/ckeditor5-engine/tests/conversion/buildviewconverter.js

@@ -23,7 +23,6 @@ import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatc
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import { convertToModelFragment, convertText } from '../../src/conversion/view-to-model-converters';
-import { stringify } from '../../src/dev-utils/model';
 
 function modelAttributesToString( item ) {
 	let result = '';
@@ -63,13 +62,13 @@ const textAttributes = [ 'linkHref', 'linkTitle', 'bold', 'italic', 'style' ];
 const pAttributes = [ 'class', 'important', 'theme', 'decorated', 'size' ];
 
 describe( 'View converter builder', () => {
-	let dispatcher, model, schema, additionalData;
+	let dispatcher, model, schema, context;
 
 	beforeEach( () => {
 		model = new Model();
 
-		// `additionalData` parameter for `.convert` calls.
-		additionalData = { context: [ '$root' ] };
+		// `context` parameter for `.convert` calls.
+		context = [ '$root' ];
 
 		schema = model.schema;
 
@@ -111,7 +110,7 @@ describe( 'View converter builder', () => {
 	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' ) ), additionalData );
+		const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ) );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
@@ -121,16 +120,52 @@ describe( 'View converter builder', () => {
 			.fromElement( 'img' )
 			.toElement( viewElement => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) );
 
-		const conversionResult = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), additionalData );
+		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' ) ), additionalData
+			new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), context
 		);
 
 		// Have to check root because result is a ModelText.
@@ -143,7 +178,7 @@ describe( 'View converter builder', () => {
 			.toAttribute( viewElement => ( { key: 'linkHref', value: viewElement.getAttribute( 'href' ) } ) );
 
 		const conversionResult = dispatcher.convert(
-			new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), additionalData
+			new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), context
 		);
 
 		// Have to check root because result is a ModelText.
@@ -158,7 +193,7 @@ describe( 'View converter builder', () => {
 			.toAttribute( viewElement => ( { key: 'class', value: viewElement.getAttribute( 'class' ) } ) );
 
 		const conversionResult = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), context
 		);
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
@@ -180,7 +215,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'p', { 'data-type': 'foo' }, new ViewText( 'xyz' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewStructure, additionalData );
+		const conversionResult = dispatcher.convert( viewStructure, context );
 
 		expect( modelToString( conversionResult ) ).to.equal(
 			'<paragraph important="true">foo</paragraph>' +
@@ -206,7 +241,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'span', { style: 'font-weight:bold; font-size:20px' }, new ViewText( 'ddd' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, context );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph><$text bold="true">aaabbbcccddd</$text></paragraph>' );
 	} );
@@ -223,12 +258,13 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, context );
 
 		const markerSearch = conversionResult.markers.get( 'search' );
 
 		expect( conversionResult.markers.size ).to.equal( 1 );
-		expect( stringify( conversionResult, markerSearch ) ).to.equal( '<paragraph>Fo[o ba]r</paragraph>' );
+		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', () => {
@@ -245,12 +281,13 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, context );
 
 		const markerSearch = conversionResult.markers.get( 'search' );
 
 		expect( conversionResult.markers.size ).to.equal( 1 );
-		expect( stringify( conversionResult, markerSearch ) ).to.equal( '<paragraph>Fo[o ba]r</paragraph>' );
+		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', () => {
@@ -271,16 +308,16 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, additionalData );
+		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( stringify( conversionResult, marker1 ) ).to.equal( '<paragraph>Fo[]o bar</paragraph>' );
-		expect( stringify( conversionResult, marker2 ) ).to.equal( '<paragraph>Foo b[]ar</paragraph>' );
-		expect( stringify( conversionResult, marker3 ) ).to.equal( '<paragraph>Foo ba[]r</paragraph>' );
+		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', () => {
@@ -288,7 +325,7 @@ describe( 'View converter builder', () => {
 
 		const element = new ViewAttributeElement( 'span' );
 
-		const result = dispatcher.convert( element, additionalData );
+		const result = dispatcher.convert( element );
 
 		expect( result ).to.be.instanceof( ModelDocumentFragment );
 		expect( result.childCount ).to.equal( 0 );
@@ -300,7 +337,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { class: 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, additionalData );
+			dispatcher.convert( element, context );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -312,7 +349,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, additionalData );
+			dispatcher.convert( element, context );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -324,7 +361,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, additionalData );
+			dispatcher.convert( element, context );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -341,7 +378,7 @@ describe( 'View converter builder', () => {
 
 		// Not quite megatron.
 		result = dispatcher.convert(
-			new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), additionalData
+			new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), context
 		);
 
 		expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
@@ -349,7 +386,7 @@ describe( 'View converter builder', () => {
 		// Almost a megatron. Missing a head.
 		result = dispatcher.convert(
 			new ViewContainerElement( 'span', { class: 'megatron', body: 'megatron', legs: 'megatron' }, new ViewText( 'foo' ) ),
-			additionalData
+			context
 		);
 
 		expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
@@ -361,7 +398,7 @@ describe( 'View converter builder', () => {
 				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
 				new ViewText( 'foo' )
 			),
-			additionalData
+			context
 		);
 
 		expect( modelToString( result ) ).to.equal( '<paragraph>foo</paragraph>' );
@@ -373,7 +410,7 @@ describe( 'View converter builder', () => {
 				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
 				new ViewText( 'foo' )
 			),
-			additionalData
+			context
 		);
 
 		expect( modelToString( result ) ).to.equal( '<MEGATRON>foo</MEGATRON>' );
@@ -393,7 +430,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'foo' )
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, context );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<span transformer="megatron">foo</span>' );
 	} );
@@ -403,7 +440,7 @@ describe( 'View converter builder', () => {
 
 		const viewElement = new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) );
 
-		const conversionResult = dispatcher.convert( viewElement, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, context );
 
 		expect( conversionResult.is( 'documentFragment' ) ).to.be.true;
 	} );
@@ -415,7 +452,7 @@ describe( 'View converter builder', () => {
 		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
 
 		const conversionResult = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), context
 		);
 
 		// Element converter was fired first even though attribute converter was added first.
@@ -431,7 +468,7 @@ describe( 'View converter builder', () => {
 		let result;
 
 		result = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), context
 		);
 
 		expect( modelToString( result ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
@@ -441,7 +478,7 @@ describe( 'View converter builder', () => {
 			.toElement( 'customP' );
 
 		result = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), context
 		);
 
 		expect( modelToString( result ) ).to.equal( '<customP>foo</customP>' );
@@ -462,7 +499,7 @@ describe( 'View converter builder', () => {
 			.toAttribute( 'size', 'small' );
 
 		const viewElement = new ViewContainerElement( 'p', { class: 'decorated small' }, new ViewText( 'foo' ) );
-		const conversionResult = dispatcher.convert( viewElement, additionalData );
+		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
@@ -485,7 +522,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'abcd', null, new ViewText( 'foo' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewStructure, additionalData );
+		const conversionResult = dispatcher.convert( viewStructure, context );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<div class="myClass"><abcd>foo</abcd></div>' );
 	} );
@@ -509,37 +546,34 @@ describe( 'View converter builder', () => {
 			)
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, context );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
 
-	// TMP We can't make this test work for now.
-	// See https://github.com/ckeditor/ckeditor5-engine/issues/1213#issuecomment-354454906
-	//
-	// 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 );
+	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;
-	// 		}
-	// 	} );
+		// Disallow bold in paragraph>$text.
+		schema.addAttributeCheck( ( ctx, attributeName ) => {
+			if ( ctx.endsWith( 'paragraph $text' ) && attributeName == 'bold' ) {
+				return false;
+			}
+		} );
 
-	// 	dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+		dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
 
-	// 	const viewElement = new ViewContainerElement( 'p', null,
-	// 		new ViewAttributeElement( 'strong', null,
-	// 			new ViewText( 'foo' )
-	// 		)
-	// 	);
+		const viewElement = new ViewContainerElement( 'p', null,
+			new ViewAttributeElement( 'strong', null,
+				new ViewText( 'foo' )
+			)
+		);
 
-	// 	const conversionResult = dispatcher.convert( viewElement, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, context );
 
-	// 	expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
-	// } );
+		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' );
@@ -551,7 +585,7 @@ describe( 'View converter builder', () => {
 			)
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, context );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
@@ -565,11 +599,11 @@ describe( 'View converter builder', () => {
 			} );
 
 		const viewElement = new ViewContainerElement( 'p' );
-		let conversionResult = dispatcher.convert( viewElement, additionalData );
+		let conversionResult = dispatcher.convert( viewElement, context );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph></paragraph>' );
 
 		viewElement.setAttribute( 'stop', true );
-		conversionResult = dispatcher.convert( viewElement, additionalData );
+		conversionResult = dispatcher.convert( viewElement, context );
 
 		expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 		expect( conversionResult.childCount ).to.equal( 0 );
@@ -587,11 +621,11 @@ describe( 'View converter builder', () => {
 		} );
 
 		const viewElement = new ViewContainerElement( 'p', { 'data-type': 'foo' } );
-		let conversionResult = dispatcher.convert( viewElement, additionalData );
+		let conversionResult = dispatcher.convert( viewElement, context );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph type="foo"></paragraph>' );
 
 		viewElement.setAttribute( 'data-type', 'stop' );
-		conversionResult = dispatcher.convert( viewElement, additionalData );
+		conversionResult = dispatcher.convert( viewElement, context );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph></paragraph>' );
 	} );
 } );

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

@@ -95,14 +95,14 @@ function viewToString( item ) {
 }
 
 describe( 'definition-based-converters', () => {
-	let model, dispatcher, modelDoc, modelRoot, viewRoot, controller, additionalData, schema;
+	let model, dispatcher, modelDoc, modelRoot, viewRoot, controller, context, schema;
 
 	beforeEach( () => {
 		model = new Model();
 	} );
 
 	function setupViewToModelTests() {
-		additionalData = { context: [ '$root' ] };
+		context = [ '$root' ];
 		schema = model.schema;
 		dispatcher = new ViewConversionDispatcher( model, { schema } );
 	}
@@ -229,7 +229,7 @@ describe( 'definition-based-converters', () => {
 				viewToModelAttribute( 'foo', { model: 'bar', view: 'strong' }, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
@@ -239,7 +239,7 @@ describe( 'definition-based-converters', () => {
 				viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'strong' } }, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
@@ -249,7 +249,7 @@ describe( 'definition-based-converters', () => {
 				viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'span', class: 'foo' } }, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), additionalData
+					new ViewAttributeElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
@@ -262,7 +262,7 @@ describe( 'definition-based-converters', () => {
 				}, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), additionalData
+					new ViewAttributeElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
@@ -275,7 +275,7 @@ describe( 'definition-based-converters', () => {
 				}, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), additionalData
+					new ViewAttributeElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
@@ -288,7 +288,7 @@ describe( 'definition-based-converters', () => {
 				}, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), additionalData
+					new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
@@ -305,7 +305,7 @@ describe( 'definition-based-converters', () => {
 				}, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), additionalData
+					new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
@@ -316,7 +316,7 @@ describe( 'definition-based-converters', () => {
 				viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'strong', priority: 'high' } }, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
@@ -396,7 +396,7 @@ describe( 'definition-based-converters', () => {
 				viewToModelElement( { model: 'bar', view: 'strong' }, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
@@ -406,7 +406,7 @@ describe( 'definition-based-converters', () => {
 				viewToModelElement( { model: 'bar', view: { name: 'strong' } }, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
@@ -416,7 +416,7 @@ describe( 'definition-based-converters', () => {
 				viewToModelElement( { model: 'bar', view: { name: 'span', class: 'foo' } }, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), additionalData
+					new ViewElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
@@ -426,7 +426,7 @@ describe( 'definition-based-converters', () => {
 				viewToModelElement( { model: 'bar', view: { name: 'span', class: [ 'foo', 'bar' ] } }, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), additionalData
+					new ViewElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
@@ -436,7 +436,7 @@ describe( 'definition-based-converters', () => {
 				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' ) ), additionalData
+					new ViewElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
@@ -446,7 +446,7 @@ describe( 'definition-based-converters', () => {
 				viewToModelElement( { model: 'bar', view: { name: 'span', attribute: { 'data-foo': 'bar' } } }, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), additionalData
+					new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
@@ -463,7 +463,7 @@ describe( 'definition-based-converters', () => {
 				}, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), additionalData
+					new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
@@ -474,7 +474,7 @@ describe( 'definition-based-converters', () => {
 				viewToModelElement( { model: 'bar', view: { name: 'strong', priority: 'high' } }, [ dispatcher ] );
 
 				const conversionResult = dispatcher.convert(
-					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), context
 				);
 
 				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );

+ 27 - 20
packages/ckeditor5-engine/tests/conversion/view-to-model-converters.js

@@ -12,11 +12,13 @@ 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, additionalData, model;
+	let dispatcher, schema, context, model;
 
 	beforeEach( () => {
 		model = new Model();
@@ -25,18 +27,18 @@ describe( 'view-to-model-converters', () => {
 		schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 		schema.extend( '$text', { allowIn: '$root' } );
 
-		additionalData = { context: [ '$root' ] };
+		context = [ '$root' ];
 
 		dispatcher = new ViewConversionDispatcher( model, { schema } );
 	} );
 
-	describe( 'convertText', () => {
+	describe( 'convertText()', () => {
 		it( 'should return converter converting ViewText to ModelText', () => {
 			const viewText = new ViewText( 'foobar' );
 
 			dispatcher.on( 'text', convertText() );
 
-			const conversionResult = dispatcher.convert( viewText, additionalData );
+			const conversionResult = dispatcher.convert( viewText );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -49,13 +51,16 @@ describe( 'view-to-model-converters', () => {
 			// 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, consumable ) => {
-				if ( consumable.consume( data.input ) ) {
-					data.output = new ModelText( data.input.data.replace( /fuck/gi, '****' ) );
+			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, additionalData );
+			const conversionResult = dispatcher.convert( viewText, context );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -72,12 +77,12 @@ describe( 'view-to-model-converters', () => {
 			const viewText = new ViewText( 'foobar' );
 			dispatcher.on( 'text', convertText() );
 
-			let conversionResult = dispatcher.convert( viewText, additionalData );
+			let conversionResult = dispatcher.convert( viewText, context );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 0 );
 
-			conversionResult = dispatcher.convert( viewText, { context: [ '$block' ] } );
+			conversionResult = dispatcher.convert( viewText, [ '$block' ] );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 1 );
@@ -90,7 +95,7 @@ describe( 'view-to-model-converters', () => {
 
 			dispatcher.on( 'text', convertText() );
 
-			const conversionResult = dispatcher.convert( viewText, additionalData );
+			const conversionResult = dispatcher.convert( viewText, context );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -98,7 +103,7 @@ describe( 'view-to-model-converters', () => {
 		} );
 	} );
 
-	describe( 'convertToModelFragment', () => {
+	describe( 'convertToModelFragment()', () => {
 		it( 'should return converter converting whole ViewDocumentFragment to ModelDocumentFragment', () => {
 			const viewFragment = new ViewDocumentFragment( [
 				new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ),
@@ -111,7 +116,7 @@ describe( 'view-to-model-converters', () => {
 			dispatcher.on( 'element', convertToModelFragment() );
 			dispatcher.on( 'documentFragment', convertToModelFragment() );
 
-			const conversionResult = dispatcher.convert( viewFragment, additionalData );
+			const conversionResult = dispatcher.convert( viewFragment, context );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.maxOffset ).to.equal( 6 );
@@ -126,17 +131,19 @@ describe( 'view-to-model-converters', () => {
 			// 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, consumable, conversionApi ) => {
-				if ( consumable.consume( data.input, { name: true } ) ) {
-					data.output = new ModelElement( 'paragraph' );
+			dispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
+				if ( conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
+					const paragraph = conversionApi.writer.createElement( 'paragraph' );
 
-					data.context.push( data.output );
-					data.output.appendChildren( conversionApi.convertChildren( data.input, consumable, data ) );
-					data.context.pop();
+					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, additionalData );
+			const conversionResult = dispatcher.convert( viewP, context );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement );

+ 375 - 114
packages/ckeditor5-engine/tests/conversion/viewconversiondispatcher.js

@@ -10,21 +10,21 @@ import ViewText from '../../src/view/text';
 
 import Model from '../../src/model/model';
 import ModelText from '../../src/model/text';
+import ModelTextProxy from '../../src/model/textproxy';
 import ModelElement from '../../src/model/element';
 import ModelDocumentFragment from '../../src/model/documentfragment';
-import { stringify } from '../../src/dev-utils/model';
+import ModelPosition from '../../src/model/position';
+import ModelRange from '../../src/model/range';
+import ModelWriter from '../../src/model/writer';
 
-import log from '@ckeditor/ckeditor5-utils/src/log';
-
-// Stored in case it is silenced and has to be restored.
-const logWarn = log.warn;
+import first from '@ckeditor/ckeditor5-utils/src/first';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 describe( 'ViewConversionDispatcher', () => {
 	let model;
 
-	afterEach( () => {
+	beforeEach( () => {
 		model = new Model();
-		log.warn = logWarn;
 	} );
 
 	describe( 'constructor()', () => {
@@ -32,23 +32,86 @@ describe( 'ViewConversionDispatcher', () => {
 			const apiObj = {};
 			const dispatcher = new ViewConversionDispatcher( model, { apiObj } );
 
-			expect( dispatcher.model ).to.equal( model );
 			expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
 			expect( dispatcher.conversionApi ).to.have.property( 'convertItem' ).that.is.instanceof( Function );
 			expect( dispatcher.conversionApi ).to.have.property( 'convertChildren' ).that.is.instanceof( Function );
+			expect( dispatcher.conversionApi ).to.have.property( 'splitToAllowedParent' ).that.is.instanceof( Function );
+		} );
+
+		it( 'should have properties', () => {
+			const dispatcher = new ViewConversionDispatcher( model );
+
+			expect( dispatcher._removeIfEmpty ).to.instanceof( Set );
 		} );
 	} );
 
-	describe( 'convert', () => {
+	describe( 'convert()', () => {
 		let dispatcher;
 
 		beforeEach( () => {
 			dispatcher = new ViewConversionDispatcher( model );
 		} );
 
-		it( 'should fire viewCleanup event on converted view part', () => {
-			silenceWarnings();
+		it( 'should create api for current conversion process', () => {
+			const viewElement = new ViewContainerElement( 'p', null, new ViewText( 'foobar' ) );
+
+			// To be sure that both converters was called.
+			const spy = sinon.spy();
+
+			// To check that the same writer instance.
+			let writer;
+
+			// Conversion process properties should be undefined/empty before conversion.
+			expect( dispatcher.conversionApi.writer ).to.not.ok;
+			expect( dispatcher.conversionApi.store ).to.not.ok;
+			expect( dispatcher._removeIfEmpty.size ).to.equal( 0 );
+
+			dispatcher.on( 'element', ( evt, data, conversionApi ) => {
+				// Check conversion api params.
+				expect( conversionApi.writer ).to.instanceof( ModelWriter );
+				expect( conversionApi.store ).to.deep.equal( {} );
+				expect( dispatcher._removeIfEmpty.size ).to.equal( 0 );
+
+				// Remember writer to check in next converter that is exactly the same instance (the same undo step).
+				writer = conversionApi.writer;
+
+				// Add some data to conversion storage to verify them in next converter.
+				conversionApi.store.foo = 'bar';
+
+				// Add empty element and mark as a split result to check in next converter.
+				dispatcher._removeIfEmpty.add( conversionApi.writer.createElement( 'paragraph' ) );
+
+				// Convert children - this will call second converter.
+				conversionApi.convertChildren( data.viewItem, data.modelCursor );
+
+				spy();
+			} );
+
+			dispatcher.on( 'text', ( evt, data, conversionApi ) => {
+				// The same writer is used in converters during one conversion.
+				expect( conversionApi.writer ).to.equal( writer );
+
+				// Data set by previous converter are remembered.
+				expect( conversionApi.store ).to.deep.equal( { foo: 'bar' } );
+
+				// Split element is remembered as well.
+				expect( dispatcher._removeIfEmpty.size ).to.equal( 1 );
+
+				spy();
+			} );
+
+			dispatcher.convert( viewElement );
+
+			// To be sure that both converters was called.
+			sinon.assert.calledTwice( spy );
+
+			// Conversion process properties should be cleared after conversion.
+			expect( dispatcher.conversionApi.writer ).to.not.ok;
+			expect( dispatcher.conversionApi.store ).to.not.ok;
+			expect( dispatcher._removeIfEmpty.size ).to.equal( 0 );
+		} );
 
+		it( 'should fire viewCleanup event on converted view part', () => {
 			sinon.spy( dispatcher, 'fire' );
 
 			const viewP = new ViewContainerElement( 'p' );
@@ -58,8 +121,6 @@ describe( 'ViewConversionDispatcher', () => {
 		} );
 
 		it( 'should fire proper events', () => {
-			silenceWarnings();
-
 			const viewText = new ViewText( 'foobar' );
 			const viewElement = new ViewContainerElement( 'p', null, viewText );
 			const viewFragment = new ViewDocumentFragment( viewElement );
@@ -79,33 +140,35 @@ describe( 'ViewConversionDispatcher', () => {
 			const spy = sinon.spy();
 			const viewText = new ViewText( 'foobar' );
 
-			dispatcher.on( 'text', ( evt, data, consumable, conversionApi ) => {
+			dispatcher.on( 'text', ( evt, data, conversionApi ) => {
 				// Check if this method has been fired.
 				spy();
 
 				// Check correctness of passed parameters.
 				expect( evt.name ).to.equal( 'text' );
-				expect( data.input ).to.equal( viewText );
-				expect( data.foo ).to.equal( 'bar' );
+				expect( data.viewItem ).to.equal( viewText );
+				expect( data.modelCursor ).to.instanceof( ModelPosition );
 
 				// Check whether consumable has appropriate value to consume.
-				expect( consumable.consume( data.input ) ).to.be.true;
+				expect( conversionApi.consumable.consume( data.viewItem ) ).to.be.true;
 
 				// Check whether conversionApi of `dispatcher` has been passed.
 				expect( conversionApi ).to.equal( dispatcher.conversionApi );
 
-				// Set conversion result to `output` property of `data`.
+				const text = conversionApi.writer.createText( data.viewItem.data );
+				conversionApi.writer.insert( text, data.modelCursor );
+
+				// Set conversion result to `modelRange` property of `data`.
 				// Later we will check if it was returned by `convert` method.
-				data.output = new ModelText( data.foo );
+				data.modelRange = ModelRange.createOn( text );
 			} );
 
-			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewText, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewText );
 
 			// Check conversion result.
 			// Result should be wrapped in document fragment.
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
-			expect( conversionResult.getChild( 0 ).data ).to.equal( 'bar' );
+			expect( conversionResult.getChild( 0 ).data ).to.equal( 'foobar' );
 			expect( spy.calledOnce ).to.be.true;
 		} );
 
@@ -113,29 +176,32 @@ describe( 'ViewConversionDispatcher', () => {
 			const spy = sinon.spy();
 			const viewElement = new ViewContainerElement( 'p', { attrKey: 'attrValue' } );
 
-			dispatcher.on( 'element', ( evt, data, consumable, conversionApi ) => {
+			dispatcher.on( 'element', ( evt, data, conversionApi ) => {
 				// Check if this method has been fired.
 				spy();
 
 				// Check correctness of passed parameters.
 				expect( evt.name ).to.equal( 'element:p' );
-				expect( data.input ).to.equal( viewElement );
-				expect( data.foo ).to.equal( 'bar' );
+				expect( data.viewItem ).to.equal( viewElement );
+				expect( data.modelCursor ).to.instanceof( ModelPosition );
 
 				// Check whether consumable has appropriate value to consume.
-				expect( consumable.consume( data.input, { name: true } ) ).to.be.true;
-				expect( consumable.consume( data.input, { attribute: 'attrKey' } ) ).to.be.true;
+				expect( conversionApi.consumable.consume( data.viewItem, { name: true } ) ).to.be.true;
+				expect( conversionApi.consumable.consume( data.viewItem, { attribute: 'attrKey' } ) ).to.be.true;
 
 				// Check whether conversionApi of `dispatcher` has been passed.
 				expect( conversionApi ).to.equal( dispatcher.conversionApi );
 
-				// Set conversion result to `output` property of `data`.
+				const paragraph = conversionApi.writer.createElement( 'paragraph' );
+				conversionApi.writer.insert( paragraph, data.modelCursor );
+
+				// Set conversion result to `modelRange` property of `data`.
 				// Later we will check if it was returned by `convert` method.
-				data.output = new ModelElement( 'paragraph' );
+				data.modelRange = ModelRange.createOn( paragraph );
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewElement, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewElement );
 
 			// Check conversion result.
 			// Result should be wrapped in document fragment.
@@ -148,28 +214,30 @@ describe( 'ViewConversionDispatcher', () => {
 			const spy = sinon.spy();
 			const viewFragment = new ViewDocumentFragment();
 
-			dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
+			dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 				// Check if this method has been fired.
 				spy();
 
 				// Check correctness of passed parameters.
 				expect( evt.name ).to.equal( 'documentFragment' );
-				expect( data.input ).to.equal( viewFragment );
-				expect( data.foo ).to.equal( 'bar' );
+				expect( data.viewItem ).to.equal( viewFragment );
+				expect( data.modelCursor ).to.instanceof( ModelPosition );
 
 				// Check whether consumable has appropriate value to consume.
-				expect( consumable.consume( data.input ) ).to.be.true;
+				expect( conversionApi.consumable.consume( data.viewItem ) ).to.be.true;
 
 				// Check whether conversionApi of `dispatcher` has been passed.
 				expect( conversionApi ).to.equal( dispatcher.conversionApi );
 
-				// Set conversion result to `output` property of `data`.
+				const text = conversionApi.writer.createText( 'foo' );
+				conversionApi.writer.insert( text, data.modelCursor );
+
+				// Set conversion result to `modelRange` property of `data`.
 				// Later we will check if it was returned by `convert` method.
-				data.output = new ModelDocumentFragment( [ new ModelText( 'foo' ) ] );
+				data.modelRange = ModelRange.createOn( text );
 			} );
 
-			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewFragment, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewFragment );
 
 			// Check conversion result.
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
@@ -177,11 +245,67 @@ describe( 'ViewConversionDispatcher', () => {
 			expect( spy.calledOnce ).to.be.true;
 		} );
 
+		it( 'should remove empty elements that was created as a result of split', () => {
+			const viewElement = new ViewContainerElement( 'p' );
+
+			// To be sure that converter was called.
+			const spy = sinon.spy();
+
+			dispatcher.on( 'element', ( evt, data, conversionApi ) => {
+				// First let's convert target element.
+				const paragraph = conversionApi.writer.createElement( 'paragraph' );
+				conversionApi.writer.insert( paragraph, data.modelCursor );
+
+				// Then add some elements and mark as split.
+
+				// Create and insert empty split element before target element.
+				const emptySplit = conversionApi.writer.createElement( 'paragraph' );
+				conversionApi.writer.insert( emptySplit, ModelPosition.createAfter( paragraph ) );
+
+				// Create and insert not empty split after target element.
+				const notEmptySplit = conversionApi.writer.createElement( 'paragraph' );
+				conversionApi.writer.appendText( 'foo', notEmptySplit );
+				conversionApi.writer.insert( notEmptySplit, ModelPosition.createAfter( emptySplit ) );
+
+				// Create and insert split with other split inside (both should be removed)
+				const outerSplit = conversionApi.writer.createElement( 'paragraph' );
+				const innerSplit = conversionApi.writer.createElement( 'paragraph' );
+				conversionApi.writer.append( innerSplit, outerSplit );
+				conversionApi.writer.insert( outerSplit, ModelPosition.createBefore( paragraph ) );
+
+				dispatcher._removeIfEmpty.add( emptySplit );
+				dispatcher._removeIfEmpty.add( notEmptySplit );
+				dispatcher._removeIfEmpty.add( outerSplit );
+				dispatcher._removeIfEmpty.add( innerSplit );
+
+				data.modelRange = ModelRange.createOn( paragraph );
+				data.modelCursor = data.modelRange.end;
+
+				// We have the following result:
+				// <p><p></p></p>[<p></p>]<p></p><p>foo</p>
+				// Everything out of selected range is a result of the split.
+
+				spy();
+			} );
+
+			const result = dispatcher.convert( viewElement );
+
+			// Empty split elements should be removed and we should have the following result:
+			// [<p></p>]<p>foo</p>
+			expect( result.childCount ).to.equal( 2 );
+			expect( result.getChild( 0 ).name ).to.equal( 'paragraph' );
+			expect( result.getChild( 0 ).childCount ).to.equal( 0 );
+			expect( result.getChild( 1 ).name ).to.equal( 'paragraph' );
+			expect( result.getChild( 1 ).childCount ).to.equal( 1 );
+			expect( result.getChild( 1 ).getChild( 0 ).data ).to.equal( 'foo' );
+		} );
+
 		it( 'should extract temporary markers elements from converter element and create static markers list', () => {
 			const viewFragment = new ViewDocumentFragment();
 
-			dispatcher.on( 'documentFragment', ( evt, data ) => {
-				data.output = new ModelDocumentFragment( [
+			dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+				// Create model fragment.
+				const fragment = new ModelDocumentFragment( [
 					new ModelText( 'fo' ),
 					new ModelElement( '$marker', { 'data-name': 'marker1' } ),
 					new ModelText( 'o' ),
@@ -190,18 +314,74 @@ describe( 'ViewConversionDispatcher', () => {
 					new ModelElement( '$marker', { 'data-name': 'marker1' } ),
 					new ModelText( 'ar' ),
 				] );
+
+				// Insert to conversion tree.
+				conversionApi.writer.insert( fragment, data.modelCursor );
+
+				// Create range on this fragment as a conversion result.
+				data.modelRange = ModelRange.createIn( data.modelCursor.parent );
 			} );
 
 			const conversionResult = dispatcher.convert( viewFragment );
 
 			expect( conversionResult.markers.size ).to.equal( 2 );
-			expect( stringify( conversionResult, conversionResult.markers.get( 'marker1' ) ) ).to.deep.equal( 'fo[ob]ar' );
-			expect( stringify( conversionResult, conversionResult.markers.get( 'marker2' ) ) ).to.deep.equal( 'foo[]bar' );
+
+			const marker1 = conversionResult.markers.get( 'marker1' );
+			const marker2 = conversionResult.markers.get( 'marker2' );
+
+			expect( marker1.start.path ).to.deep.equal( [ 2 ] );
+			expect( marker1.end.path ).to.deep.equal( [ 4 ] );
+			expect( marker2.start.path ).to.deep.equal( marker2.end.path ).to.deep.equal( [ 3 ] );
+		} );
+
+		it( 'should convert according to given context', () => {
+			dispatcher = new ViewConversionDispatcher( model, { schema: model.schema } );
+
+			const spy = sinon.spy();
+			const viewElement = new ViewContainerElement( 'third' );
+			let checkChildResult;
+
+			model.schema.register( 'first', {
+				allowIn: '$root'
+			} );
+			model.schema.register( 'second', {
+				allowIn: 'first'
+			} );
+			model.schema.register( 'third', {
+				allowIn: 'second',
+				disallowIn: 'first'
+			} );
+
+			dispatcher.on( 'element:third', ( evt, data, conversionApi ) => {
+				spy();
+				checkChildResult = conversionApi.schema.checkChild( data.modelCursor, 'third' );
+			} );
+
+			// Default context $root.
+			dispatcher.convert( viewElement );
+			sinon.assert.calledOnce( spy );
+			expect( checkChildResult ).to.false;
+
+			// SchemaDefinition as context.
+			dispatcher.convert( viewElement, [ 'first' ] );
+			sinon.assert.calledTwice( spy );
+			expect( checkChildResult ).to.false;
+
+			// Position as context.
+			const fragment = new ModelDocumentFragment( [
+				new ModelElement( 'first', { foo: 'bar' }, [
+					new ModelElement( 'second', null )
+				] )
+			] );
+
+			dispatcher.convert( viewElement, new ModelPosition( fragment, [ 0, 0, 0 ] ) );
+			sinon.assert.calledThrice( spy );
+			expect( checkChildResult ).to.true;
 		} );
 	} );
 
 	describe( 'conversionApi', () => {
-		let spy, spyP, spyText, viewP, viewText, modelP, modelText, consumableMock, dispatcher,
+		let spy, spyP, spyText, viewP, viewText, modelP, modelText, rootMock, dispatcher,
 			spyNull, spyArray, viewDiv, viewNull, viewArray;
 
 		beforeEach( () => {
@@ -214,60 +394,69 @@ describe( 'ViewConversionDispatcher', () => {
 			modelP = new ModelElement( 'paragraph' );
 			modelText = new ModelText( 'foobar' );
 
-			consumableMock = {};
+			// 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 );
+			dispatcher = new ViewConversionDispatcher( model, { schema: model.schema } );
 
-			dispatcher.on( 'element:p', ( evt, data, consumable ) => {
+			dispatcher.on( 'element:p', ( evt, data ) => {
 				spyP();
 
-				expect( data.foo ).to.equal( 'bar' );
-				expect( consumable ).to.equal( consumableMock );
-
-				data.output = modelP;
+				data.modelRange = ModelRange.createOn( modelP );
+				data.modelCursor = data.modelRange.end;
 			} );
 
-			dispatcher.on( 'text', ( evt, data, consumable ) => {
+			dispatcher.on( 'text', ( evt, data ) => {
 				spyText();
 
-				expect( data.foo ).to.equal( 'bar' );
-				expect( consumable ).to.equal( consumableMock );
-
-				data.output = modelText;
+				data.modelRange = ModelRange.createOn( modelText );
+				data.modelCursor = data.modelRange.end;
 			} );
 
 			spyNull = sinon.spy();
 			spyArray = sinon.spy();
 
 			viewDiv = new ViewContainerElement( 'div' ); // Will not be recognized and not converted.
-			viewNull = new ViewContainerElement( 'null' ); // Will return `null` in `data.output` upon conversion.
-			viewArray = new ViewContainerElement( 'array' ); // Will return an array in `data.output` upon conversion.
+			viewNull = new ViewContainerElement( 'null' ); // Will return `null` in `data.modelRange` upon conversion.
+			viewArray = new ViewContainerElement( 'array' ); // Will return an array in `data.modelRange` upon conversion.
 
 			dispatcher.on( 'element:null', ( evt, data ) => {
 				spyNull();
 
-				data.output = null;
+				data.modelRange = null;
 			} );
 
 			dispatcher.on( 'element:array', ( evt, data ) => {
 				spyArray();
 
-				data.output = [ new ModelText( 'foo' ) ];
+				data.modelRange = [ new ModelText( 'foo' ) ];
 			} );
 		} );
 
-		describe( 'convertItem', () => {
-			it( 'should pass consumable and additional data to proper converter and return data.output', () => {
-				silenceWarnings();
-
-				dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
+		describe( 'convertItem()', () => {
+			it( 'should call proper converter and return correct data', () => {
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 					spy();
 
-					expect( conversionApi.convertItem( viewP, consumableMock, data ) ).to.equal( modelP );
-					expect( conversionApi.convertItem( viewText, consumableMock, data ) ).to.equal( modelText );
+					const pResult = conversionApi.convertItem( viewP, data.modelCursor );
+					expect( pResult.modelRange ).instanceof( ModelRange );
+					expect( pResult.modelRange.start.path ).to.deep.equal( [ 0 ] );
+					expect( pResult.modelRange.end.path ).to.deep.equal( [ 1 ] );
+					expect( first( pResult.modelRange.getItems() ) ).to.equal( modelP );
+					expect( pResult.modelCursor ).to.instanceof( ModelPosition );
+					expect( pResult.modelCursor.path ).to.deep.equal( [ 1 ] );
+
+					const textResult = conversionApi.convertItem( viewText, data.modelCursor );
+					expect( textResult.modelRange ).instanceof( ModelRange );
+					expect( textResult.modelRange.start.path ).to.deep.equal( [ 1 ] );
+					expect( textResult.modelRange.end.path ).to.deep.equal( [ 7 ] );
+					expect( first( textResult.modelRange.getItems() ) ).to.instanceof( ModelTextProxy );
+					expect( first( textResult.modelRange.getItems() ).data ).to.equal( 'foobar' );
+					expect( textResult.modelCursor ).to.instanceof( ModelPosition );
+					expect( textResult.modelCursor.path ).to.deep.equal( [ 7 ] );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment(), { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment() );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyP.calledOnce ).to.be.true;
@@ -275,95 +464,167 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			it( 'should do nothing if element was not converted', () => {
-				sinon.stub( log, 'warn' );
-
-				dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 					spy();
 
-					expect( conversionApi.convertItem( viewDiv ) ).to.equal( null );
-					expect( conversionApi.convertItem( viewNull ) ).to.equal( null );
+					expect( conversionApi.convertItem( viewDiv, data.modelCursor ).modelRange ).to.equal( null );
+					expect( conversionApi.convertItem( viewNull, data.modelCursor ).modelRange ).to.equal( null );
 				} );
 
 				dispatcher.convert( new ViewDocumentFragment() );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyNull.calledOnce ).to.be.true;
-				expect( log.warn.called ).to.be.false;
-
-				log.warn.restore();
 			} );
 
-			it( 'should return null if element was incorrectly converted and log a warning', () => {
-				sinon.stub( log, 'warn' );
-
-				dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
+			it( 'should throw an error if element was incorrectly converted', () => {
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 					spy();
 
-					expect( conversionApi.convertItem( viewArray ) ).to.equal( null );
+					conversionApi.convertItem( viewArray, data.modelCursor );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment() );
+				expect( () => {
+					dispatcher.convert( new ViewDocumentFragment() );
+				} ).to.throw( CKEditorError, /^view-conversion-dispatcher-incorrect-result/ );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyArray.calledOnce ).to.be.true;
-				expect( log.warn.calledOnce ).to.be.true;
-
-				log.warn.restore();
 			} );
 		} );
 
-		describe( 'convertChildren', () => {
+		describe( 'convertChildren()', () => {
 			it( 'should fire conversion for all children of passed element and return conversion results ' +
 				'wrapped in document fragment', () => {
-				silenceWarnings();
-
-				dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 					spy();
 
-					const result = conversionApi.convertChildren( data.input, consumableMock, data );
+					const result = conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( rootMock ) );
 
-					expect( result ).to.be.instanceof( ModelDocumentFragment );
-					expect( result.childCount ).to.equal( 2 );
-					expect( result.getChild( 0 ) ).to.equal( modelP );
-					expect( result.getChild( 1 ) ).to.equal( modelText );
+					expect( result.modelRange ).to.be.instanceof( ModelRange );
+					expect( result.modelRange.start.path ).to.deep.equal( [ 0 ] );
+					expect( result.modelRange.end.path ).to.deep.equal( [ 7 ] );
+					expect( Array.from( result.modelRange.getItems() ) ).to.length( 2 );
+					expect( Array.from( result.modelRange.getItems() )[ 0 ] ).to.equal( modelP );
+					expect( Array.from( result.modelRange.getItems() )[ 1 ] ).to.instanceof( ModelTextProxy );
+					expect( Array.from( result.modelRange.getItems() )[ 1 ].data ).to.equal( 'foobar' );
+
+					expect( result.modelCursor ).instanceof( ModelPosition );
+					expect( result.modelCursor.path ).to.deep.equal( [ 7 ] );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ) );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyP.calledOnce ).to.be.true;
 				expect( spyText.calledOnce ).to.be.true;
 			} );
+		} );
+
+		describe( 'splitToAllowedParent()', () => {
+			beforeEach( () => {
+				model.schema.register( 'paragraph', {
+					allowIn: '$root'
+				} );
+			} );
+
+			it( 'should return current position if element is allowed on this position', () => {
+				const spy = sinon.spy();
+
+				model.schema.register( 'span', {
+					allowIn: 'paragraph'
+				} );
+
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					const paragraph = conversionApi.writer.createElement( 'paragraph' );
+					const span = conversionApi.writer.createElement( 'span' );
+					const position = ModelPosition.createAt( paragraph );
+
+					const result = conversionApi.splitToAllowedParent( span, position );
+
+					expect( result ).to.deep.equal( { position } );
+					spy();
+				} );
+
+				dispatcher.convert( new ViewDocumentFragment() );
+				sinon.assert.calledOnce( spy );
+			} );
+
+			it( 'should split position to allowed ancestor if element is allowed in one of ancestors', () => {
+				const spy = sinon.spy();
+
+				model.schema.register( 'section', {
+					allowIn: '$root'
+				} );
+				model.schema.register( 'span', {
+					allowIn: 'paragraph'
+				} );
+				model.schema.extend( 'paragraph', {
+					allowIn: 'section'
+				} );
 
-			it( 'should filter out incorrectly converted elements and log warnings', () => {
-				sinon.stub( log, 'warn' );
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					const section = conversionApi.writer.createElement( 'section' );
+					const paragraph = conversionApi.writer.createElement( 'paragraph' );
+					const span = conversionApi.writer.createElement( 'span' );
+					conversionApi.writer.insert( paragraph, section );
+					conversionApi.writer.insert( span, paragraph );
+
+					const position = ModelPosition.createAt( span );
+
+					const paragraph2 = conversionApi.writer.createElement( 'paragraph' );
+					const result = conversionApi.splitToAllowedParent( paragraph2, position );
+
+					expect( result ).to.deep.equal( {
+						position: ModelPosition.createAfter( paragraph ),
+						cursorParent: paragraph.parent.getChild( 1 ).getChild( 0 )
+					} );
 
-				dispatcher.on( 'documentFragment', ( evt, data, consumable, conversionApi ) => {
 					spy();
+				} );
+
+				dispatcher.convert( new ViewDocumentFragment() );
+				sinon.assert.calledOnce( spy );
+			} );
+
+			it( 'should return null if element is not allowed in position and any of ancestors', () => {
+				const spy = sinon.spy();
 
-					const result = conversionApi.convertChildren( data.input, consumableMock, data );
+				model.schema.register( 'span' );
 
-					expect( result ).to.be.instanceof( ModelDocumentFragment );
-					expect( result.childCount ).to.equal( 2 );
-					expect( result.getChild( 0 ) ).to.equal( modelP );
-					expect( result.getChild( 1 ) ).to.equal( modelText );
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					const paragraph = conversionApi.writer.createElement( 'paragraph' );
+					const span = conversionApi.writer.createElement( 'span' );
+					const position = ModelPosition.createAt( paragraph );
+
+					const result = conversionApi.splitToAllowedParent( span, position );
+
+					expect( result ).to.null;
+					spy();
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment() );
+				sinon.assert.calledOnce( spy );
+			} );
 
-				expect( spy.calledOnce ).to.be.true;
-				expect( spyNull.calledOnce ).to.be.true;
-				expect( spyArray.calledOnce ).to.be.true;
-				expect( log.warn.calledOnce ).to.be.true;
+			it( 'should return null if element is not allowed in position and any of ancestors but is allowed in context tree', () => {
+				const spy = sinon.spy();
 
-				log.warn.restore();
+				model.schema.register( 'div', {
+					allowIn: '$root',
+				} );
+
+				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
+					const code = conversionApi.writer.createElement( 'div' );
+					const result = conversionApi.splitToAllowedParent( code, data.modelCursor );
+
+					expect( result ).to.null;
+					spy();
+				} );
+
+				dispatcher.convert( new ViewDocumentFragment(), [ '$root', 'paragraph' ] );
+				sinon.assert.calledOnce( spy );
 			} );
 		} );
 	} );
-
-	// Silences warnings that pop up in tests. Use when the test checks a specific functionality and we are not interested in those logs.
-	// No need to restore `log.warn` - it is done in `afterEach()`.
-	function silenceWarnings() {
-		log.warn = () => {};
-	}
 } );

+ 4 - 9
packages/ckeditor5-engine/tests/dev-utils/model.js

@@ -432,13 +432,7 @@ describe( 'model test utils', () => {
 		} );
 
 		test( 'creates empty DocumentFragment with selection', {
-			data: '[]',
-			check( fragment, selection ) {
-				expect( fragment ).to.be.instanceOf( DocumentFragment );
-				expect( fragment.childCount ).to.equal( 0 );
-				expect( selection.rangeCount ).to.equal( 1 );
-				expect( selection.getFirstRange().isEqual( Range.createFromParentsAndOffsets( fragment, 0, fragment, 0 ) ) ).to.be.true;
-			}
+			data: '[]'
 		} );
 
 		test( 'returns Element if range is around single element', {
@@ -529,7 +523,7 @@ describe( 'model test utils', () => {
 		it( 'throws when try to set element not registered in schema', () => {
 			expect( () => {
 				parse( '<xyz></xyz>', model.schema );
-			} ).to.throw( Error, 'Element \'xyz\' was not allowed in context ["$root"].' );
+			} ).to.throw( Error, 'Element \'xyz\' was not allowed in given position.' );
 		} );
 
 		it( 'throws when try to set text directly to $root without registering it', () => {
@@ -537,7 +531,7 @@ describe( 'model test utils', () => {
 
 			expect( () => {
 				parse( 'text', model.schema );
-			} ).to.throw( Error, 'Text was not allowed in context ["$root"].' );
+			} ).to.throw( Error, 'Text was not allowed in given position.' );
 		} );
 
 		it( 'converts data in the specified context', () => {
@@ -652,6 +646,7 @@ describe( 'model test utils', () => {
 		function test( title, options ) {
 			it( title, () => {
 				const output = options.output || options.data;
+
 				const data = parse( options.data, model.schema );
 				let converted, selection;
 

+ 17 - 0
packages/ckeditor5-engine/tests/model/differ.js

@@ -533,6 +533,23 @@ describe( 'Differ', () => {
 			] );
 		} );
 
+		it( 'on an element - only one of many attributes changes', () => {
+			const range = Range.createFromParentsAndOffsets( root, 0, root.getChild( 0 ), 0 );
+
+			// Set an attribute on an element. It won't change afterwards.
+			attribute( range, 'otherAttr', null, true );
+
+			// "Flush" differ.
+			differ.getChanges();
+			differ.reset();
+
+			attribute( range, attributeKey, attributeOldValue, attributeNewValue );
+
+			expectChanges( [
+				{ type: 'attribute', range, attributeKey, attributeOldValue, attributeNewValue }
+			] );
+		} );
+
 		it( 'on a character', () => {
 			const parent = root.getChild( 1 );
 			const range = Range.createFromParentsAndOffsets( parent, 1, parent, 2 );

+ 140 - 0
packages/ckeditor5-engine/tests/model/schema.js

@@ -377,6 +377,17 @@ describe( 'Schema', () => {
 			expect( schema.checkChild( root1, '$text' ) ).to.be.false;
 		} );
 
+		it( 'accepts a schemaContext instance as a context', () => {
+			const rootContext = new SchemaContext( Position.createAt( root1 ) );
+			const paragraphContext = new SchemaContext( Position.createAt( r1p1 ) );
+
+			expect( schema.checkChild( rootContext, 'paragraph' ) ).to.be.true;
+			expect( schema.checkChild( rootContext, '$text' ) ).to.be.false;
+
+			expect( schema.checkChild( paragraphContext, '$text' ) ).to.be.true;
+			expect( schema.checkChild( paragraphContext, 'paragraph' ) ).to.be.false;
+		} );
+
 		it( 'accepts a position as a context', () => {
 			const posInRoot = Position.createAt( root1 );
 			const posInParagraph = Position.createAt( r1p1 );
@@ -466,6 +477,14 @@ describe( 'Schema', () => {
 			expect( schema.checkAttribute( posInParagraph, 'align' ) ).to.be.true;
 		} );
 
+		it( 'accepts a schemaContext instance as a context', () => {
+			const rootContext = new SchemaContext( Position.createAt( root1 ) );
+			const paragraphContext = new SchemaContext( Position.createAt( r1p1 ) );
+
+			expect( schema.checkAttribute( rootContext, 'align' ) ).to.be.false;
+			expect( schema.checkAttribute( paragraphContext, 'align' ) ).to.be.true;
+		} );
+
 		it( 'accepts an array of node names as a context', () => {
 			const contextInRoot = [ '$root' ];
 			const contextInParagraph = [ '$root', 'paragraph' ];
@@ -1353,6 +1372,59 @@ describe( 'Schema', () => {
 		}
 	} );
 
+	describe( 'findAllowedParent()', () => {
+		beforeEach( () => {
+			schema.register( '$root' );
+			schema.register( 'blockQuote', {
+				allowIn: '$root'
+			} );
+			schema.register( 'paragraph', {
+				allowIn: 'blockQuote'
+			} );
+			schema.register( '$text', {
+				allowIn: 'paragraph'
+			} );
+		} );
+
+		it( 'should return position ancestor that allows to insert given note to it', () => {
+			const node = new Element( 'paragraph' );
+
+			const allowedParent = schema.findAllowedParent( node, Position.createAt( r1bQp ) );
+
+			expect( allowedParent ).to.equal( r1bQ );
+		} );
+
+		it( 'should return position ancestor that allows to insert given note to it when position is already i such an element', () => {
+			const node = new Text( 'text' );
+
+			const parent = schema.findAllowedParent( node, Position.createAt( r1bQp ) );
+
+			expect( parent ).to.equal( r1bQp );
+		} );
+
+		it( 'should return null when limit element will be reached before allowed parent', () => {
+			schema.extend( 'blockQuote', {
+				isLimit: true
+			} );
+			schema.register( 'div', {
+				allowIn: '$root'
+			} );
+			const node = new Element( 'div' );
+
+			const parent = schema.findAllowedParent( node, Position.createAt( r1bQp ) );
+
+			expect( parent ).to.null;
+		} );
+
+		it( 'should return null when there is no allowed ancestor for given position', () => {
+			const node = new Element( 'section' );
+
+			const parent = schema.findAllowedParent( node, Position.createAt( r1bQp ) );
+
+			expect( parent ).to.null;
+		} );
+	} );
+
 	describe( 'removeDisallowedAttributes()', () => {
 		let model, doc, root;
 
@@ -2604,6 +2676,14 @@ describe( 'SchemaContext', () => {
 			expect( Array.from( ctx.getItem( 2 ).getAttributeKeys() ).sort() ).to.deep.equal( [ 'align' ] );
 		} );
 
+		it( 'creates context based on a SchemaContext instance', () => {
+			const previousCtx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			const ctx = new SchemaContext( previousCtx );
+
+			expect( ctx ).to.equal( previousCtx );
+		} );
+
 		it( 'filters out DocumentFragment when it is a first item of context - array', () => {
 			const ctx = new SchemaContext( [ new DocumentFragment(), 'paragraph' ] );
 
@@ -2675,6 +2755,66 @@ describe( 'SchemaContext', () => {
 		} );
 	} );
 
+	describe( 'concat()', () => {
+		it( 'creates new SchemaContext instance with new item - #string', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			const newCtx = ctx.concat( 'd' );
+
+			expect( newCtx ).to.instanceof( SchemaContext );
+			expect( newCtx ).to.not.equal( ctx );
+			expect( Array.from( newCtx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c', 'd' ] );
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+		} );
+
+		it( 'creates new SchemaContext instance with new item - #text', () => {
+			const node = new Text( 'd' );
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			const newCtx = ctx.concat( node );
+
+			expect( newCtx ).to.instanceof( SchemaContext );
+			expect( newCtx ).to.not.equal( ctx );
+			expect( Array.from( newCtx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c', '$text' ] );
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+		} );
+
+		it( 'creates new SchemaContext instance with new item - #node', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+			const parent = new Element( 'parent', null, new Element( 'd' ) );
+
+			const newCtx = ctx.concat( parent.getChild( 0 ) );
+
+			expect( newCtx ).to.instanceof( SchemaContext );
+			expect( newCtx ).to.not.equal( ctx );
+			expect( Array.from( newCtx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c', 'd' ] );
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+		} );
+
+		it( 'creates new SchemaContext instance with new item - #SchemaContext', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+			const schemaContext = new SchemaContext( [ 'd', 'e' ] );
+
+			const newCtx = ctx.concat( schemaContext );
+
+			expect( newCtx ).to.instanceof( SchemaContext );
+			expect( newCtx ).to.not.equal( ctx );
+			expect( Array.from( newCtx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c', 'd', 'e' ] );
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+		} );
+
+		it( 'creates new SchemaContext instance with new item - #array', () => {
+			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );
+
+			const newCtx = ctx.concat( [ 'd', new Text( 'e' ), new Element( 'f' ) ] );
+
+			expect( newCtx ).to.instanceof( SchemaContext );
+			expect( newCtx ).to.not.equal( ctx );
+			expect( Array.from( newCtx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c', 'd', '$text', 'f' ] );
+			expect( Array.from( ctx.getNames() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+		} );
+	} );
+
 	describe( 'getNames()', () => {
 		it( 'returns an iterator', () => {
 			const ctx = new SchemaContext( [ 'a', 'b', 'c' ] );

+ 57 - 0
packages/ckeditor5-engine/tests/model/utils/insertcontent.js

@@ -407,6 +407,63 @@ describe( 'DataController utils', () => {
 						'<paragraph>b</paragraph>'
 					);
 				} );
+
+				it( 'should not merge a paragraph wrapped in blockQuote with list item (checking left merge)', () => {
+					model.schema.register( 'blockQuote', {
+						allowWhere: '$block',
+						allowContentOf: '$root',
+					} );
+
+					setData( model, '<listItem>fo[]o</listItem>' );
+
+					insertHelper( '<blockQuote><paragraph>xxx</paragraph></blockQuote><heading1>yyy</heading1>' );
+
+					expect( getData( model ) ).to.equal(
+						'<listItem>fo</listItem>' +
+						'<blockQuote>' +
+						'<paragraph>xxx</paragraph>' +
+						'</blockQuote>' +
+						'<heading1>yyy[]o</heading1>'
+					);
+				} );
+
+				it( 'should not merge a paragraph wrapped in blockQuote with list item (checking right merge)', () => {
+					model.schema.register( 'blockQuote', {
+						allowWhere: '$block',
+						allowContentOf: '$root',
+					} );
+
+					setData( model, '<listItem>fo[]o</listItem>' );
+
+					insertHelper( '<heading1>yyy</heading1><blockQuote><paragraph>xxx</paragraph></blockQuote>' );
+
+					expect( getData( model ) ).to.equal(
+						'<listItem>foyyy</listItem>' +
+						'<blockQuote>' +
+						'<paragraph>xxx</paragraph>' +
+						'</blockQuote>' +
+						'<listItem>[]o</listItem>'
+					);
+				} );
+
+				it( 'should not merge a paragraph wrapped in blockQuote with list item (checking both merges)', () => {
+					model.schema.register( 'blockQuote', {
+						allowWhere: '$block',
+						allowContentOf: '$root',
+					} );
+
+					setData( model, '<listItem>fo[]o</listItem>' );
+
+					insertHelper( '<blockQuote><paragraph>xxx</paragraph></blockQuote>' );
+
+					expect( getData( model ) ).to.equal(
+						'<listItem>fo</listItem>' +
+						'<blockQuote>' +
+						'<paragraph>xxx</paragraph>' +
+						'</blockQuote>' +
+						'<listItem>[]o</listItem>'
+					);
+				} );
 			} );
 
 			describe( 'mixed content to block', () => {

+ 38 - 2
packages/ckeditor5-engine/tests/model/writer.js

@@ -1790,6 +1790,42 @@ describe( 'Writer', () => {
 			} ).to.throw( CKEditorError, /^writer-split-element-no-parent/ );
 		} );
 
+		it( 'should split elements to limitElement', () => {
+			const div = new Element( 'div', null, p );
+			const section = new Element( 'section', null, div );
+
+			root.insertChildren( 0, section );
+
+			split( new Position( p, [ 3 ] ), section );
+
+			expect( root.maxOffset ).to.equal( 1 );
+			expect( section.maxOffset ).to.equal( 2 );
+
+			expect( section.getChild( 0 ).name ).to.equal( 'div' );
+			expect( section.getChild( 0 ).getChild( 0 ).name ).to.equal( 'p' );
+			expect( section.getChild( 0 ).getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
+			expect( count( section.getChild( 0 ).getChild( 0 ).getAttributes() ) ).to.equal( 1 );
+			expect( section.getChild( 0 ).getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
+
+			expect( section.getChild( 1 ).name ).to.equal( 'div' );
+			expect( section.getChild( 1 ).getChild( 0 ).name ).to.equal( 'p' );
+			expect( section.getChild( 1 ).getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
+			expect( count( section.getChild( 1 ).getChild( 0 ).getAttributes() ) ).to.equal( 1 );
+			expect( section.getChild( 1 ).getChild( 0 ).getChild( 0 ).data ).to.equal( 'bar' );
+		} );
+
+		it( 'should throw when limitElement is not a position ancestor', () => {
+			const div = new Element( 'div', null, p );
+			const section = new Element( 'section', null, div );
+
+			root.insertChildren( 0, div );
+			root.insertChildren( 1, section );
+
+			expect( () => {
+				split( new Position( p, [ 3 ] ), section );
+			} ).to.throw( CKEditorError, /^writer-split-invalid-limit-element/ );
+		} );
+
 		it( 'should throw when trying to use detached writer', () => {
 			const writer = new Writer( model, batch );
 
@@ -2299,9 +2335,9 @@ describe( 'Writer', () => {
 		} );
 	}
 
-	function split( position ) {
+	function split( position, limitElement ) {
 		model.enqueueChange( batch, writer => {
-			writer.split( position );
+			writer.split( position, limitElement );
 		} );
 	}