Przeglądaj źródła

Refactored conversion and DataController to use new Batch API.

Oskar Wróbel 8 lat temu
rodzic
commit
02b01ab398

+ 7 - 5
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -198,7 +198,7 @@ export default class DataController {
 			const batch = this.model.batch( 'transparent' );
 
 			batch.remove( ModelRange.createIn( modelRoot ) );
-			batch.insert( this.parse( data ), modelRoot );
+			batch.insert( this.parse( data, batch ), modelRoot );
 		} );
 	}
 
@@ -208,16 +208,17 @@ export default class DataController {
 	 *
 	 * @see #set
 	 * @param {String} data Data to parse.
+	 * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
 	 * @param {String} [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, batch, context = '$root' ) {
 		// data -> view
 		const viewDocumentFragment = this.processor.toView( data );
 
 		// view -> model
-		return this.toModel( viewDocumentFragment, context );
+		return this.toModel( viewDocumentFragment, batch, context );
 	}
 
 	/**
@@ -231,12 +232,13 @@ export default class DataController {
 	 *
 	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
 	 * Element or document fragment which content will be converted.
+	 * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
 	 * @param {String} [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, batch, context = '$root' ) {
+		return this.viewToModel.convert( viewElementOrFragment, { context: [ context ], batch } );
 	}
 
 	/**

+ 3 - 3
packages/ckeditor5-engine/src/controller/insertcontent.js

@@ -229,7 +229,7 @@ class Insertion {
 		// If the node is a text and bare text is allowed in current position it means that the node
 		// contains disallowed attributes and we have to remove them.
 		else if ( this.schema.check( { name: '$text', inside: this.position } ) ) {
-			this.schema.removeDisallowedAttributes( [ node ], this.position );
+			this.schema.removeDisallowedAttributes( [ node ], this.position, this.batch );
 			this._handleNode( node, context );
 		}
 		// If text is not allowed, try autoparagraphing.
@@ -341,7 +341,7 @@ class Insertion {
 	 * @param {Object} context
 	 */
 	_tryAutoparagraphing( node, context ) {
-		const paragraph = new Element( 'paragraph' );
+		const paragraph = this.batch.createElement( 'paragraph' );
 
 		// Do not autoparagraph if the paragraph won't be allowed there,
 		// cause that would lead to an infinite loop. The paragraph would be rejected in
@@ -350,7 +350,7 @@ class Insertion {
 			// When node is a text and is disallowed by schema it means that contains disallowed attributes
 			// and we need to remove them.
 			if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) {
-				this.schema.removeDisallowedAttributes( [ node ], [ paragraph ] );
+				this.schema.removeDisallowedAttributes( [ node ], [ paragraph ], this.batch );
 			}
 
 			if ( this._checkIsAllowed( node, [ paragraph ] ) ) {

+ 12 - 9
packages/ckeditor5-engine/src/conversion/buildviewconverter.js

@@ -8,9 +8,6 @@
  */
 
 import Matcher from '../view/matcher';
-import ModelElement from '../model/element';
-import ModelPosition from '../model/position';
-import modelWriter from '../model/writer';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 
@@ -266,13 +263,15 @@ class ViewConverterBuilder {
 	 *		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
 	 *		buildViewConverter().for( dispatcher )
 	 *			.fromElement( 'img' )
-	 *			.toElement( ( viewElement ) => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } );
+	 *			.toElement( ( viewElement, batch ) => batch.createElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) );
 	 *
 	 * @param {String|Function} element Model element name or model element creator function.
 	 */
 	toElement( element ) {
 		function eventCallbackGen( from ) {
 			return ( evt, data, consumable, conversionApi ) => {
+				const batch = data.batch;
+
 				// 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 );
@@ -285,7 +284,7 @@ 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 ) : new ModelElement( element );
+					const modelElement = element instanceof Function ? element( data.input, batch ) : batch.createElement( element );
 
 					// Do not convert if element building function returned falsy value.
 					if ( !modelElement ) {
@@ -310,8 +309,10 @@ class ViewConverterBuilder {
 
 					// Convert children of converted view element and append them to `modelElement`.
 					const modelChildren = conversionApi.convertChildren( data.input, consumable, data );
-					const insertPosition = ModelPosition.createAt( modelElement, 'end' );
-					modelWriter.insert( insertPosition, modelChildren );
+
+					for ( const child of Array.from( modelChildren ) ) {
+						batch.append( child, modelElement );
+					}
 
 					// Remove created `modelElement` from the parents stack.
 					data.context.pop();
@@ -434,6 +435,8 @@ class ViewConverterBuilder {
 	toMarker( creator ) {
 		function eventCallbackGen( from ) {
 			return ( evt, data, consumable ) => {
+				const batch = data.batch;
+
 				// 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 );
@@ -450,7 +453,7 @@ class ViewConverterBuilder {
 					modelElement = creator( data.input );
 				// When there is no creator then create model element basing on data from view element.
 				} else {
-					modelElement = new ModelElement( '$marker', { 'data-name': data.input.getAttribute( 'data-name' ) } );
+					modelElement = batch.createElement( '$marker', { 'data-name': data.input.getAttribute( 'data-name' ) } );
 				}
 
 				// Check if model element is correct (has proper name and property).
@@ -525,7 +528,7 @@ function setAttributeOn( toChange, attribute, data, conversionApi ) {
 	};
 
 	if ( conversionApi.schema.check( schemaQuery ) ) {
-		toChange.setAttribute( attribute.key, attribute.value );
+		data.batch.setAttribute( toChange, attribute.key, attribute.value );
 	}
 }
 

+ 2 - 8
packages/ckeditor5-engine/src/conversion/view-to-model-converters.js

@@ -3,10 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-import ModelDocumentFragment from '../model/documentfragment';
-import ModelText from '../model/text';
-import { normalizeNodes } from '../model/writer';
-
 /**
  * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
  * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}.
@@ -33,9 +29,7 @@ export function convertToModelFragment() {
 	return ( evt, data, consumable, conversionApi ) => {
 		// Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
 		if ( !data.output && consumable.consume( data.input, { name: true } ) ) {
-			const convertedChildren = conversionApi.convertChildren( data.input, consumable, data );
-
-			data.output = new ModelDocumentFragment( normalizeNodes( convertedChildren ) );
+			data.output = conversionApi.convertChildren( data.input, consumable, data );
 		}
 	};
 }
@@ -54,7 +48,7 @@ export function convertText() {
 
 		if ( conversionApi.schema.check( schemaQuery ) ) {
 			if ( consumable.consume( data.input ) ) {
-				data.output = new ModelText( data.input.data );
+				data.output = data.batch.createText( data.input.data );
 			}
 		}
 	};

+ 30 - 26
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -13,7 +13,6 @@ import ModelPosition from '../model/position';
 import ModelTreeWalker from '../model/treewalker';
 import ModelNode from '../model/node';
 import ModelDocumentFragment from '../model/documentfragment';
-import { remove } from '../model/writer';
 
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
@@ -134,13 +133,16 @@ 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`
+	 * @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/batch~Batch} additionalData.batch Batch to which the deltas will be added.
 	 * @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, additionalData ) {
+		const batch = additionalData.batch;
+
 		this.fire( 'viewCleanup', viewItem );
 
 		const consumable = ViewConsumable.createFrom( viewItem );
@@ -149,16 +151,19 @@ export default class ViewConversionDispatcher {
 		// 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 new ModelDocumentFragment();
+			return batch.createDocumentFragment();
 		}
 
 		// When conversion result is not a document fragment we need to wrap it in document fragment.
 		if ( !conversionResult.is( 'documentFragment' ) ) {
-			conversionResult = new ModelDocumentFragment( [ conversionResult ] );
+			const docFrag = batch.createDocumentFragment();
+
+			batch.append( conversionResult, docFrag );
+			conversionResult = docFrag;
 		}
 
 		// Extract temporary markers elements from model and set as static markers collection.
-		conversionResult.markers = extractMarkersFromModelFragment( conversionResult );
+		conversionResult.markers = extractMarkersFromModelFragment( conversionResult, batch );
 
 		return conversionResult;
 	}
@@ -203,24 +208,23 @@ export default class ViewConversionDispatcher {
 	 * @private
 	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren
 	 */
-	_convertChildren( input, consumable, additionalData = {} ) {
-		// Get all children of view input item.
-		const viewChildren = Array.from( input.getChildren() );
-
-		// 1. Map those children to model.
-		// 2. Filter out items that has not been converted or for which conversion returned wrong result (for those warning is logged).
-		// 3. Extract children from document fragments to flatten results.
-		const convertedChildren = viewChildren
-			.map( viewChild => this._convertItem( viewChild, consumable, additionalData ) )
-			.filter( converted => converted instanceof ModelNode || converted instanceof ModelDocumentFragment )
-			.reduce( ( result, filtered ) => {
-				return result.concat(
-					filtered.is( 'documentFragment' ) ? Array.from( filtered.getChildren() ) : filtered
-				);
-			}, [] );
-
-		// Normalize array to model document fragment.
-		return new ModelDocumentFragment( convertedChildren );
+	_convertChildren( input, consumable, additionalData ) {
+		const batch = additionalData.batch;
+		const documentFragment = batch.createDocumentFragment();
+
+		for ( const viewChild of Array.from( input.getChildren() ) ) {
+			const modelChild = this._convertItem( viewChild, consumable, additionalData );
+
+			if ( modelChild instanceof ModelNode ) {
+				batch.append( modelChild, documentFragment );
+			} else if ( modelChild instanceof ModelDocumentFragment ) {
+				for ( const child of Array.from( modelChild ) ) {
+					batch.append( child, documentFragment );
+				}
+			}
+		}
+
+		return documentFragment;
 	}
 
 	/**
@@ -278,7 +282,7 @@ mix( ViewConversionDispatcher, EmitterMixin );
 //
 // @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
 // @returns {Map<String, module:engine/model/range~Range>} List of static markers.
-function extractMarkersFromModelFragment( modelItem ) {
+function extractMarkersFromModelFragment( modelItem, batch ) {
 	const markerElements = new Set();
 	const markers = new Map();
 
@@ -310,7 +314,7 @@ function extractMarkersFromModelFragment( modelItem ) {
 		}
 
 		// Remove marker element from DocumentFragment.
-		remove( ModelRange.createOn( markerElement ) );
+		batch.remove( markerElement );
 	}
 
 	return markers;

+ 7 - 6
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -96,8 +96,10 @@ export function setData( document, data, options = {} ) {
 	let modelDocumentFragment, selection;
 	const modelRoot = document.getRoot( options.rootName || 'main' );
 
+	const batch = document.batch( options.batchType || 'transparent' );
+
 	// Parse data string to model.
-	const parsedResult = setData._parse( data, document.schema, {
+	const parsedResult = setData._parse( data, document.schema, batch, {
 		lastRangeBackward: options.lastRangeBackward,
 		selectionAttributes: options.selectionAttributes,
 		context: [ modelRoot.name ]
@@ -113,8 +115,6 @@ export function setData( document, data, options = {} ) {
 
 	document.enqueueChanges( () => {
 		// Replace existing model in document by new one.
-		const batch = document.batch( options.batchType || 'transparent' );
-
 		batch.remove( ModelRange.createIn( modelRoot ) );
 		batch.insert( modelDocumentFragment, modelRoot );
 
@@ -243,7 +243,8 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
  *
  * @param {String} data HTML-like string to be parsed.
  * @param {module:engine/model/schema~Schema} schema Schema instance uses by converters for element validation.
- * @param {Object} options Additional configuration.
+ * @param {module:engine/model/batch~Batch} batch Batch used for conversion.
+ * @param {Object} [options={}] Additional configuration.
  * @param {Array<Object>} [options.selectionAttributes] List of attributes which will be passed to the selection.
  * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward.
  * @param {module:engine/model/schema~SchemaPath} [options.context=[ '$root' ]] The conversion context.
@@ -252,7 +253,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
  * module:engine/model/documentfragment~DocumentFragment|Object} Returns parsed model node or
  * object with two fields `model` and `selection` when selection ranges were included in data to parse.
  */
-export function parse( data, schema, options = {} ) {
+export function parse( data, schema, batch, options = {} ) {
 	const mapper = new Mapper();
 
 	// Replace not accepted by XML `$text` tag name by valid one `model-text-with-attributes`.
@@ -283,7 +284,7 @@ export function parse( data, schema, options = {} ) {
 	viewToModel.on( 'text', convertToModelText() );
 
 	// Convert view to model.
-	let model = viewToModel.convert( viewDocumentFragment.root, { context: options.context || [ '$root' ] } );
+	let model = viewToModel.convert( viewDocumentFragment.root, { context: options.context || [ '$root' ], batch } );
 
 	// If root DocumentFragment contains only one element - return that element.
 	if ( model.is( 'documentFragment' ) && model.childCount == 1 ) {

+ 1 - 2
packages/ckeditor5-engine/src/model/document.js

@@ -168,9 +168,8 @@ export default class Document {
 		if ( operation.isDocumentOperation ) {
 			this.version++;
 			this.history.addDelta( operation.delta );
+			this.fire( 'change', operation.type, changes, operation.delta.batch, operation.delta.type );
 		}
-
-		this.fire( 'change', operation.type, changes, operation.delta.batch, operation.delta.type );
 	}
 
 	/**

+ 3 - 10
packages/ckeditor5-engine/src/model/schema.js

@@ -414,14 +414,11 @@ export default class Schema {
 	}
 
 	/**
-	 * Removes disallowed by {@link module:engine/model/schema~Schema schema} attributes from given nodes.
-	 * When {@link module:engine/model/batch~Batch batch} parameter is provided then attributes will be removed
-	 * using that batch, by creating {@link module:engine/model/delta/attributedelta~AttributeDelta attribute deltas}.
-	 * Otherwise, attributes will be removed directly from provided nodes using {@link module:engine/model/node~Node node} API.
+	 * Removes disallowed by {@link module:engine/model/schema~Schema schema} attributes from given nodes..
 	 *
 	 * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes that will be filtered.
 	 * @param {module:engine/model/schema~SchemaPath} inside Path inside which schema will be checked.
-	 * @param {module:engine/model/batch~Batch} [batch] Batch to which the deltas will be added.
+	 * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
 	 */
 	removeDisallowedAttributes( nodes, inside, batch ) {
 		for ( const node of nodes ) {
@@ -435,11 +432,7 @@ export default class Schema {
 				// TODO: this should be improved to check all combination of attributes.
 				for ( const attribute of node.getAttributeKeys() ) {
 					if ( !this.check( { name, attributes: attribute, inside: queryPath } ) ) {
-						if ( batch ) {
-							batch.removeAttribute( node, attribute );
-						} else {
-							node.removeAttribute( attribute );
-						}
+						batch.removeAttribute( node, attribute );
 					}
 				}
 			}

+ 22 - 14
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -48,7 +48,7 @@ describe( 'DataController', () => {
 	describe( 'parse', () => {
 		it( 'should set text', () => {
 			schema.allow( { name: '$text', inside: '$root' } );
-			const model = data.parse( '<p>foo<b>bar</b></p>' );
+			const model = data.parse( '<p>foo<b>bar</b></p>', modelDocument.batch() );
 
 			expect( model ).to.instanceof( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( 'foobar' );
@@ -59,7 +59,7 @@ describe( 'DataController', () => {
 
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 
-			const model = data.parse( '<p>foo<b>bar</b></p>' );
+			const model = data.parse( '<p>foo<b>bar</b></p>', modelDocument.batch() );
 
 			expect( model ).to.instanceof( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( '<paragraph>foobar</paragraph>' );
@@ -70,7 +70,7 @@ describe( 'DataController', () => {
 
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 
-			const model = data.parse( '<p>foo</p><p>bar</p>' );
+			const model = data.parse( '<p>foo</p><p>bar</p>', modelDocument.batch() );
 
 			expect( model ).to.instanceof( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
@@ -83,20 +83,20 @@ describe( 'DataController', () => {
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 			buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', true );
 
-			const model = data.parse( '<p>foo<b>bar</b></p>' );
+			const model = data.parse( '<p>foo<b>bar</b></p>', modelDocument.batch() );
 
 			expect( model ).to.instanceof( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 		} );
 
 		it( 'should parse in the root context by default', () => {
-			const model = data.parse( 'foo' );
+			const model = data.parse( 'foo', modelDocument.batch() );
 
 			expect( stringify( model ) ).to.equal( '' );
 		} );
 
 		it( 'should accept parsing context', () => {
-			const model = data.parse( 'foo', '$block' );
+			const model = data.parse( 'foo', modelDocument.batch(), '$block' );
 
 			expect( stringify( model ) ).to.equal( 'foo' );
 		} );
@@ -111,7 +111,7 @@ describe( 'DataController', () => {
 
 		it( 'should convert content of an element #1', () => {
 			const viewElement = parseView( '<p>foo</p>' );
-			const model = data.toModel( viewElement );
+			const model = data.toModel( viewElement, modelDocument.batch() );
 
 			expect( model ).to.instanceof( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( '<paragraph>foo</paragraph>' );
@@ -119,7 +119,7 @@ describe( 'DataController', () => {
 
 		it( 'should convert content of an element #2', () => {
 			const viewFragment = parseView( '<p>foo</p><p>bar</p>' );
-			const model = data.toModel( viewFragment );
+			const model = data.toModel( viewFragment, modelDocument.batch() );
 
 			expect( model ).to.be.instanceOf( ModelDocumentFragment );
 			expect( stringify( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
@@ -134,10 +134,10 @@ describe( 'DataController', () => {
 			const viewFragment = new ViewDocumentFragment( [ parseView( 'foo' ) ] );
 
 			// Model fragment in root.
-			expect( stringify( data.toModel( viewFragment ) ) ).to.equal( '' );
+			expect( stringify( data.toModel( viewFragment, modelDocument.batch() ) ) ).to.equal( '' );
 
 			// Model fragment in inline root.
-			expect( stringify( data.toModel( viewFragment, 'inlineRoot' ) ) ).to.equal( 'foo' );
+			expect( stringify( data.toModel( viewFragment, modelDocument.batch(), 'inlineRoot' ) ) ).to.equal( 'foo' );
 		} );
 	} );
 
@@ -264,6 +264,8 @@ describe( 'DataController', () => {
 	} );
 
 	describe( 'stringify', () => {
+		let batch;
+
 		beforeEach( () => {
 			modelDocument.schema.registerItem( 'paragraph', '$block' );
 			modelDocument.schema.registerItem( 'div' );
@@ -272,16 +274,18 @@ describe( 'DataController', () => {
 			modelDocument.schema.allow( { name: 'div', inside: '$root' } );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
+
+			batch = modelDocument.batch();
 		} );
 
 		it( 'should stringify a content of an element', () => {
-			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', modelDocument.schema );
+			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', modelDocument.schema, batch );
 
 			expect( data.stringify( modelElement ) ).to.equal( '<p>foo</p>' );
 		} );
 
 		it( 'should stringify a content of a document fragment', () => {
-			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', modelDocument.schema );
+			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', modelDocument.schema, batch );
 
 			expect( data.stringify( modelDocumentFragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
 		} );
@@ -299,7 +303,7 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should convert a content of an element', () => {
-			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', modelDocument.schema );
+			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', modelDocument.schema, modelDocument.batch() );
 
 			const viewDocumentFragment = data.toView( modelElement );
 
@@ -313,7 +317,11 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should convert a document fragment', () => {
-			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', modelDocument.schema );
+			const modelDocumentFragment = parseModel(
+				'<paragraph>foo</paragraph><paragraph>bar</paragraph>',
+				modelDocument.schema,
+				modelDocument.batch()
+			);
 
 			const viewDocumentFragment = data.toView( modelDocumentFragment );
 

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

@@ -155,7 +155,8 @@ describe( 'EditingController', () => {
 				'<paragraph>foo</paragraph>' +
 				'<paragraph></paragraph>' +
 				'<paragraph>bar</paragraph>',
-				model.schema
+				model.schema,
+				model.batch()
 			)._children );
 
 			model.enqueueChanges( () => {
@@ -409,9 +410,11 @@ describe( 'EditingController', () => {
 
 			editing.destroy();
 
+			const batch = model.batch();
+
 			model.enqueueChanges( () => {
-				const modelData = parse( '<paragraph>foo</paragraph>', model.schema ).getChild( 0 );
-				model.batch().insert( modelData, model.getRoot() );
+				const modelData = parse( '<paragraph>foo</paragraph>', model.schema, batch ).getChild( 0 );
+				batch.insert( modelData, model.getRoot() );
 			} );
 
 			expect( spy.called ).to.be.false;

+ 4 - 2
packages/ckeditor5-engine/tests/controller/insertcontent.js

@@ -756,12 +756,14 @@ describe( 'DataController', () => {
 	//
 	// @param {module:engine/model/item~Item|String} content
 	function insertHelper( content ) {
+		const batch = doc.batch();
+
 		if ( typeof content == 'string' ) {
-			content = parse( content, doc.schema, {
+			content = parse( content, doc.schema, batch, {
 				context: [ '$clipboardHolder' ]
 			} );
 		}
 
-		insertContent( dataController, content, doc.selection );
+		insertContent( dataController, content, doc.selection, batch );
 	}
 } );

+ 20 - 19
packages/ckeditor5-engine/tests/conversion/advanced-converters.js

@@ -40,12 +40,13 @@ import { convertToModelFragment, convertText } from '../../src/conversion/view-t
 import { createRangeOnElementOnly } from '../../tests/model/_utils/utils';
 
 describe( 'advanced-converters', () => {
-	let modelDoc, modelRoot, viewRoot, mapper, modelDispatcher, viewDispatcher;
+	let modelDoc, modelRoot, viewRoot, mapper, modelDispatcher, viewDispatcher, batch;
 
 	beforeEach( () => {
 		modelDoc = new ModelDocument();
 		modelRoot = modelDoc.createRoot();
 		viewRoot = new ViewContainerElement( 'div' );
+		batch = modelDoc.batch();
 
 		mapper = new Mapper();
 		mapper.bindElements( modelRoot, viewRoot );
@@ -207,8 +208,8 @@ describe( 'advanced-converters', () => {
 
 			const viewFigureConverter = function( evt, data, consumable, conversionApi ) {
 				if ( consumable.consume( data.input, { name: true } ) ) {
-					const modelImage = conversionApi.convertItem( data.input.getChild( 0 ), consumable );
-					const modelCaption = conversionApi.convertItem( data.input.getChild( 1 ), consumable );
+					const modelImage = conversionApi.convertItem( data.input.getChild( 0 ), consumable, { batch } );
+					const modelCaption = conversionApi.convertItem( data.input.getChild( 1 ), consumable, { batch } );
 
 					modelImage.appendChildren( modelCaption );
 
@@ -231,7 +232,7 @@ describe( 'advanced-converters', () => {
 			const viewFigcaptionConverter = function( evt, data, consumable, conversionApi ) {
 				if ( consumable.consume( data.input, { name: true } ) ) {
 					const modelCaption = new ModelElement( 'caption' );
-					const children = conversionApi.convertChildren( data.input, consumable );
+					const children = conversionApi.convertChildren( data.input, consumable, { batch } );
 
 					modelCaption.appendChildren( children );
 
@@ -286,7 +287,7 @@ describe( 'advanced-converters', () => {
 
 		it( 'should convert view image to model', () => {
 			const viewElement = new ViewContainerElement( 'img', { src: 'bar.jpg', title: 'bar' } );
-			const modelElement = viewDispatcher.convert( viewElement );
+			const modelElement = viewDispatcher.convert( viewElement, { batch } );
 
 			expect( modelToString( modelElement ) ).to.equal( '<image src="bar.jpg" title="bar"></image>' );
 		} );
@@ -300,7 +301,7 @@ describe( 'advanced-converters', () => {
 					new ViewContainerElement( 'figcaption', null, new ViewText( 'foobar' ) )
 				]
 			);
-			const modelElement = viewDispatcher.convert( viewElement );
+			const modelElement = viewDispatcher.convert( viewElement, { batch } );
 
 			expect( modelToString( modelElement ) ).to.equal( '<image src="bar.jpg" title="bar"><caption>foobar</caption></image>' );
 		} );
@@ -371,7 +372,7 @@ describe( 'advanced-converters', () => {
 			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 );
+						data.output = conversionApi.convertChildren( data.input, consumable, { batch } );
 					}
 
 					for ( const child of data.output ) {
@@ -383,7 +384,7 @@ describe( 'advanced-converters', () => {
 			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 );
+						data.output = conversionApi.convertChildren( data.input, consumable, { batch } );
 					}
 
 					for ( const child of data.output ) {
@@ -468,7 +469,7 @@ describe( 'advanced-converters', () => {
 						}
 					}
 
-					const children = conversionApi.convertChildren( data.input, consumable );
+					const children = conversionApi.convertChildren( data.input, consumable, { batch } );
 					data.output.appendChildren( children );
 				}
 			} );
@@ -519,7 +520,7 @@ describe( 'advanced-converters', () => {
 		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 );
+			const modelText = viewDispatcher.convert( viewElement, { batch } ).getChild( 0 );
 
 			expect( modelText ).to.be.instanceof( ModelText );
 			expect( modelText.data ).to.equal( 'foo' );
@@ -590,7 +591,7 @@ describe( 'advanced-converters', () => {
 				]
 			);
 
-			const modelElement = viewDispatcher.convert( viewElement );
+			const modelElement = viewDispatcher.convert( viewElement, { batch } );
 
 			expect( modelToString( modelElement ) ).to.equal( '<quote linkHref="foo.html" linkTitle="Foo source">foo</quote>' );
 		} );
@@ -602,7 +603,7 @@ describe( 'advanced-converters', () => {
 		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 );
+					data.output = conversionApi.convertChildren( data.input, consumable, { batch } );
 				}
 
 				for ( const child of data.output ) {
@@ -615,7 +616,7 @@ describe( 'advanced-converters', () => {
 			if ( consumable.consume( data.input, { name: true } ) ) {
 				data.output = new ModelElement( 'paragraph' );
 
-				const children = conversionApi.convertChildren( data.input, consumable );
+				const children = conversionApi.convertChildren( data.input, consumable, { batch } );
 
 				for ( let i = 1; i < children.childCount; i++ ) {
 					const child = children.getChild( i );
@@ -632,13 +633,13 @@ describe( 'advanced-converters', () => {
 
 		viewDispatcher.on( 'element:table', ( evt, data, consumable, conversionApi ) => {
 			if ( consumable.consume( data.input, { name: true } ) ) {
-				data.output = conversionApi.convertChildren( data.input, consumable );
+				data.output = conversionApi.convertChildren( data.input, consumable, { batch } );
 			}
 		} );
 
 		viewDispatcher.on( 'element:td', ( evt, data, consumable, conversionApi ) => {
 			if ( consumable.consume( data.input, { name: true } ) ) {
-				data.output = conversionApi.convertChildren( data.input, consumable );
+				data.output = conversionApi.convertChildren( data.input, consumable, { batch } );
 			}
 		} );
 
@@ -653,7 +654,7 @@ describe( 'advanced-converters', () => {
 			] )
 		] );
 
-		const model = viewDispatcher.convert( viewTable );
+		const model = viewDispatcher.convert( viewTable, { batch } );
 		const modelFragment = new ModelDocumentFragment( model );
 
 		expect( modelToString( modelFragment ) )
@@ -680,7 +681,7 @@ describe( 'advanced-converters', () => {
 						}
 					}
 
-					data.output.appendChildren( conversionApi.convertChildren( data.input, consumable ) );
+					data.output.appendChildren( conversionApi.convertChildren( data.input, consumable, { batch } ) );
 				}
 			}, { priority: 'lowest' } );
 
@@ -704,7 +705,7 @@ describe( 'advanced-converters', () => {
 			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 );
+						data.output = conversionApi.convertChildren( data.input, consumable, { batch } );
 					}
 
 					for ( const child of data.output ) {
@@ -755,7 +756,7 @@ describe( 'advanced-converters', () => {
 				] )
 			] );
 
-			const modelElement = viewDispatcher.convert( viewElement );
+			const modelElement = viewDispatcher.convert( viewElement, { batch } );
 
 			expect( modelToString( modelElement ) ).to.equal(
 				'<table cellpadding="5" cellspacing="5">' +

+ 38 - 33
packages/ckeditor5-engine/tests/conversion/buildviewconverter.js

@@ -6,6 +6,7 @@
 import buildViewConverter from '../../src/conversion/buildviewconverter';
 
 import ModelSchema from '../../src/model/schema';
+import ModelDocument from '../../src/model/document';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 import ModelElement from '../../src/model/element';
 import ModelTextProxy from '../../src/model/textproxy';
@@ -63,11 +64,15 @@ const textAttributes = [ undefined, 'linkHref', 'linkTitle', 'bold', 'italic', '
 const pAttributes = [ undefined, 'class', 'important', 'theme', 'decorated', 'size' ];
 
 describe( 'View converter builder', () => {
-	let dispatcher, schema, objWithContext;
+	let dispatcher, schema, additionalData, batch;
+
+	const modelDocument = new ModelDocument();
 
 	beforeEach( () => {
+		batch = modelDocument.batch();
+
 		// `additionalData` parameter for `.convert` calls.
-		objWithContext = { context: [ '$root' ] };
+		additionalData = { context: [ '$root' ], batch };
 
 		schema = new ModelSchema();
 
@@ -95,7 +100,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' ) ), objWithContext );
+		const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
@@ -105,7 +110,7 @@ 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' } ), objWithContext );
+		const conversionResult = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<image src="foo.jpg"></image>' );
 	} );
@@ -114,7 +119,7 @@ describe( 'View converter builder', () => {
 		buildViewConverter().for( dispatcher ).fromElement( 'strong' ).toAttribute( 'bold', true );
 
 		const conversionResult = dispatcher.convert(
-			new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), objWithContext
+			new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
 		);
 
 		// Have to check root because result is a ModelText.
@@ -127,7 +132,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' ) ), objWithContext
+			new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		// Have to check root because result is a ModelText.
@@ -142,7 +147,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' ) ), objWithContext
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
@@ -164,7 +169,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'p', { 'data-type': 'foo' }, new ViewText( 'xyz' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewStructure, objWithContext );
+		const conversionResult = dispatcher.convert( viewStructure, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal(
 			'<paragraph important="true">foo</paragraph>' +
@@ -190,7 +195,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'span', { style: 'font-weight:bold; font-size:20px' }, new ViewText( 'ddd' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph><$text bold="true">aaabbbcccddd</$text></paragraph>' );
 	} );
@@ -207,7 +212,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		const markerSearch = conversionResult.markers.get( 'search' );
 
@@ -229,7 +234,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		const markerSearch = conversionResult.markers.get( 'search' );
 
@@ -255,7 +260,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		const marker1 = conversionResult.markers.get( 'marker1' );
 		const marker2 = conversionResult.markers.get( 'marker2' );
@@ -272,7 +277,7 @@ describe( 'View converter builder', () => {
 
 		const element = new ViewAttributeElement( 'span' );
 
-		const result = dispatcher.convert( element, objWithContext );
+		const result = dispatcher.convert( element, additionalData );
 
 		expect( result ).to.be.instanceof( ModelDocumentFragment );
 		expect( result.childCount ).to.equal( 0 );
@@ -284,7 +289,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { class: 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, objWithContext );
+			dispatcher.convert( element, additionalData );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -296,7 +301,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, objWithContext );
+			dispatcher.convert( element, additionalData );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -308,7 +313,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, objWithContext );
+			dispatcher.convert( element, additionalData );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -325,7 +330,7 @@ describe( 'View converter builder', () => {
 
 		// Not quite megatron.
 		result = dispatcher.convert(
-			new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), objWithContext
+			new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
@@ -333,7 +338,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' ) ),
-			objWithContext
+			additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
@@ -345,7 +350,7 @@ describe( 'View converter builder', () => {
 				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
 				new ViewText( 'foo' )
 			),
-			objWithContext
+			additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<paragraph>foo</paragraph>' );
@@ -357,7 +362,7 @@ describe( 'View converter builder', () => {
 				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
 				new ViewText( 'foo' )
 			),
-			objWithContext
+			additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<MEGATRON>foo</MEGATRON>' );
@@ -377,7 +382,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'foo' )
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<span transformer="megatron">foo</span>' );
 	} );
@@ -387,7 +392,7 @@ describe( 'View converter builder', () => {
 
 		const viewElement = new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) );
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( conversionResult.is( 'documentFragment' ) ).to.be.true;
 	} );
@@ -399,7 +404,7 @@ describe( 'View converter builder', () => {
 		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
 
 		const conversionResult = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		// Element converter was fired first even though attribute converter was added first.
@@ -415,7 +420,7 @@ describe( 'View converter builder', () => {
 		let result;
 
 		result = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
@@ -425,7 +430,7 @@ describe( 'View converter builder', () => {
 			.toElement( 'customP' );
 
 		result = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<customP>foo</customP>' );
@@ -446,7 +451,7 @@ describe( 'View converter builder', () => {
 			.toAttribute( 'size', 'small' );
 
 		const viewElement = new ViewContainerElement( 'p', { class: 'decorated small' }, new ViewText( 'foo' ) );
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		// 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
@@ -469,7 +474,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'abcd', null, new ViewText( 'foo' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewStructure, objWithContext );
+		const conversionResult = dispatcher.convert( viewStructure, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<div class="myClass"><abcd>foo</abcd></div>' );
 	} );
@@ -488,7 +493,7 @@ describe( 'View converter builder', () => {
 			)
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
@@ -507,7 +512,7 @@ describe( 'View converter builder', () => {
 			)
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, objWithContext );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
@@ -521,11 +526,11 @@ describe( 'View converter builder', () => {
 			} );
 
 		const viewElement = new ViewContainerElement( 'p' );
-		let conversionResult = dispatcher.convert( viewElement, objWithContext );
+		let conversionResult = dispatcher.convert( viewElement, additionalData );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph></paragraph>' );
 
 		viewElement.setAttribute( 'stop', true );
-		conversionResult = dispatcher.convert( viewElement, objWithContext );
+		conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 		expect( conversionResult.childCount ).to.equal( 0 );
@@ -543,11 +548,11 @@ describe( 'View converter builder', () => {
 		} );
 
 		const viewElement = new ViewContainerElement( 'p', { 'data-type': 'foo' } );
-		let conversionResult = dispatcher.convert( viewElement, objWithContext );
+		let conversionResult = dispatcher.convert( viewElement, additionalData );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph type="foo"></paragraph>' );
 
 		viewElement.setAttribute( 'data-type', 'stop' );
-		conversionResult = dispatcher.convert( viewElement, objWithContext );
+		conversionResult = dispatcher.convert( viewElement, additionalData );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph></paragraph>' );
 	} );
 } );

+ 13 - 9
packages/ckeditor5-engine/tests/conversion/view-to-model-converters.js

@@ -9,6 +9,7 @@ import ViewDocumentFragment from '../../src/view/documentfragment';
 import ViewText from '../../src/view/text';
 
 import ModelSchema from '../../src/model/schema';
+import ModelDocument from '../../src/model/document';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 import ModelElement from '../../src/model/element';
 import ModelText from '../../src/model/text';
@@ -16,13 +17,16 @@ import ModelText from '../../src/model/text';
 import { convertToModelFragment, convertText } from '../../src/conversion/view-to-model-converters';
 
 describe( 'view-to-model-converters', () => {
-	let dispatcher, schema, objWithContext;
+	let dispatcher, schema, additionalData, batch;
+
+	const modelDocument = new ModelDocument();
 
 	beforeEach( () => {
 		schema = new ModelSchema();
 		schema.registerItem( 'paragraph', '$block' );
 		schema.allow( { name: '$text', inside: '$root' } );
-		objWithContext = { context: [ '$root' ] };
+		batch = modelDocument.batch();
+		additionalData = { context: [ '$root' ], batch };
 		dispatcher = new ViewConversionDispatcher( { schema } );
 	} );
 
@@ -32,7 +36,7 @@ describe( 'view-to-model-converters', () => {
 
 			dispatcher.on( 'text', convertText() );
 
-			const conversionResult = dispatcher.convert( viewText, objWithContext );
+			const conversionResult = dispatcher.convert( viewText, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -51,7 +55,7 @@ describe( 'view-to-model-converters', () => {
 				}
 			} );
 
-			const conversionResult = dispatcher.convert( viewText, objWithContext );
+			const conversionResult = dispatcher.convert( viewText, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -64,12 +68,12 @@ describe( 'view-to-model-converters', () => {
 			const viewText = new ViewText( 'foobar' );
 			dispatcher.on( 'text', convertText() );
 
-			let conversionResult = dispatcher.convert( viewText, objWithContext );
+			let conversionResult = dispatcher.convert( viewText, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 0 );
 
-			conversionResult = dispatcher.convert( viewText, { context: [ '$block' ] } );
+			conversionResult = dispatcher.convert( viewText, { context: [ '$block' ], batch } );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 1 );
@@ -82,7 +86,7 @@ describe( 'view-to-model-converters', () => {
 
 			dispatcher.on( 'text', convertText() );
 
-			const conversionResult = dispatcher.convert( viewText, objWithContext );
+			const conversionResult = dispatcher.convert( viewText, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -103,7 +107,7 @@ describe( 'view-to-model-converters', () => {
 			dispatcher.on( 'element', convertToModelFragment() );
 			dispatcher.on( 'documentFragment', convertToModelFragment() );
 
-			const conversionResult = dispatcher.convert( viewFragment, objWithContext );
+			const conversionResult = dispatcher.convert( viewFragment, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.maxOffset ).to.equal( 6 );
@@ -128,7 +132,7 @@ describe( 'view-to-model-converters', () => {
 				}
 			} );
 
-			const conversionResult = dispatcher.convert( viewP, objWithContext );
+			const conversionResult = dispatcher.convert( viewP, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement );

+ 24 - 15
packages/ckeditor5-engine/tests/conversion/viewconversiondispatcher.js

@@ -11,6 +11,7 @@ import ViewText from '../../src/view/text';
 import ModelText from '../../src/model/text';
 import ModelElement from '../../src/model/element';
 import ModelDocumentFragment from '../../src/model/documentfragment';
+import ModelDocument from '../../src/model/document';
 import { stringify } from '../../src/dev-utils/model';
 
 import log from '@ckeditor/ckeditor5-utils/src/log';
@@ -35,10 +36,13 @@ describe( 'ViewConversionDispatcher', () => {
 	} );
 
 	describe( 'convert', () => {
-		let dispatcher;
+		let dispatcher, batch;
+
+		const modelDocument = new ModelDocument();
 
 		beforeEach( () => {
 			dispatcher = new ViewConversionDispatcher();
+			batch = modelDocument.batch();
 		} );
 
 		it( 'should fire viewCleanup event on converted view part', () => {
@@ -47,7 +51,7 @@ describe( 'ViewConversionDispatcher', () => {
 			sinon.spy( dispatcher, 'fire' );
 
 			const viewP = new ViewContainerElement( 'p' );
-			dispatcher.convert( viewP );
+			dispatcher.convert( viewP, { batch } );
 
 			expect( dispatcher.fire.calledWith( 'viewCleanup', viewP ) ).to.be.true;
 		} );
@@ -61,9 +65,9 @@ describe( 'ViewConversionDispatcher', () => {
 
 			sinon.spy( dispatcher, 'fire' );
 
-			dispatcher.convert( viewText );
-			dispatcher.convert( viewElement );
-			dispatcher.convert( viewFragment );
+			dispatcher.convert( viewText, { batch } );
+			dispatcher.convert( viewElement, { batch } );
+			dispatcher.convert( viewFragment, { batch } );
 
 			expect( dispatcher.fire.calledWith( 'text' ) ).to.be.true;
 			expect( dispatcher.fire.calledWith( 'element:p' ) ).to.be.true;
@@ -95,7 +99,7 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewText, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewText, { foo: 'bar', batch } );
 
 			// Check conversion result.
 			// Result should be wrapped in document fragment.
@@ -130,7 +134,7 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewElement, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewElement, { foo: 'bar', batch } );
 
 			// Check conversion result.
 			// Result should be wrapped in document fragment.
@@ -164,7 +168,7 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewFragment, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewFragment, { foo: 'bar', batch } );
 
 			// Check conversion result.
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
@@ -187,7 +191,7 @@ describe( 'ViewConversionDispatcher', () => {
 				] );
 			} );
 
-			const conversionResult = dispatcher.convert( viewFragment );
+			const conversionResult = dispatcher.convert( viewFragment, { batch } );
 
 			expect( conversionResult.markers.size ).to.equal( 2 );
 			expect( stringify( conversionResult, conversionResult.markers.get( 'marker1' ) ) ).to.deep.equal( 'fo[ob]ar' );
@@ -197,9 +201,13 @@ describe( 'ViewConversionDispatcher', () => {
 
 	describe( 'conversionApi', () => {
 		let spy, spyP, spyText, viewP, viewText, modelP, modelText, consumableMock, dispatcher,
-			spyNull, spyArray, viewDiv, viewNull, viewArray;
+			spyNull, spyArray, viewDiv, viewNull, viewArray, batch;
+
+		const modelDocument = new ModelDocument();
 
 		beforeEach( () => {
+			batch = modelDocument.batch();
+
 			spy = sinon.spy();
 			spyP = sinon.spy();
 			spyText = sinon.spy();
@@ -260,9 +268,10 @@ describe( 'ViewConversionDispatcher', () => {
 
 					expect( conversionApi.convertItem( viewP, consumableMock, data ) ).to.equal( modelP );
 					expect( conversionApi.convertItem( viewText, consumableMock, data ) ).to.equal( modelText );
+					expect( data.batch ).to.equal( batch );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment(), { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment(), { foo: 'bar', batch } );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyP.calledOnce ).to.be.true;
@@ -279,7 +288,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( conversionApi.convertItem( viewNull ) ).to.equal( null );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment() );
+				dispatcher.convert( new ViewDocumentFragment(), { batch } );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyNull.calledOnce ).to.be.true;
@@ -297,7 +306,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( conversionApi.convertItem( viewArray ) ).to.equal( null );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment() );
+				dispatcher.convert( new ViewDocumentFragment(), { batch } );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyArray.calledOnce ).to.be.true;
@@ -323,7 +332,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( result.getChild( 1 ) ).to.equal( modelText );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), { foo: 'bar', batch } );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyP.calledOnce ).to.be.true;
@@ -344,7 +353,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( result.getChild( 1 ) ).to.equal( modelText );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), { foo: 'bar', batch } );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyNull.calledOnce ).to.be.true;

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

@@ -497,21 +497,21 @@ describe( 'model test utils', () => {
 
 		it( 'throws when invalid XML', () => {
 			expect( () => {
-				parse( '<a><b></a></b>', document.schema );
+				parse( '<a><b></a></b>', document.schema, document.batch() );
 			} ).to.throw( Error, /Parse error/ );
 		} );
 
 		it( 'throws when try to set element not registered in schema', () => {
 			expect( () => {
-				parse( '<xyz></xyz>', document.schema );
+				parse( '<xyz></xyz>', document.schema, document.batch() );
 			} ).to.throw( Error, 'Element \'xyz\' not allowed in context ["$root"].' );
 		} );
 
 		it( 'throws when try to set text directly to $root without registering it', () => {
-			const doc = new Document();
+			const document = new Document();
 
 			expect( () => {
-				parse( 'text', doc.schema );
+				parse( 'text', document.schema, document.batch() );
 			} ).to.throw( Error, 'Element \'$text\' not allowed in context ["$root"].' );
 		} );
 
@@ -521,7 +521,7 @@ describe( 'model test utils', () => {
 			doc.schema.allow( { name: '$text', inside: 'foo' } );
 
 			expect( () => {
-				parse( 'text', doc.schema, { context: [ 'foo' ] } );
+				parse( 'text', doc.schema, doc.batch(), { context: [ 'foo' ] } );
 			} ).to.not.throw();
 		} );
 
@@ -556,7 +556,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets selection attributes', () => {
-				const result = parse( 'foo[]bar', document.schema, { selectionAttributes: {
+				const result = parse( 'foo[]bar', document.schema, document.batch(), { selectionAttributes: {
 					bold: true,
 					italic: true
 				} } );
@@ -577,7 +577,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets selection with attribute containing an element', () => {
-				const result = parse( 'x[<a></a>]', document.schema, { selectionAttributes: {
+				const result = parse( 'x[<a></a>]', document.schema, document.batch(), { selectionAttributes: {
 					bold: true
 				} } );
 
@@ -586,7 +586,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets a backward selection containing an element', () => {
-				const result = parse( 'x[<a></a>]', document.schema, {
+				const result = parse( 'x[<a></a>]', document.schema, document.batch(), {
 					lastRangeBackward: true
 				} );
 
@@ -599,7 +599,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets selection within a text with different attributes', () => {
-				const result = parse( '<$text bold="true">fo[o</$text>ba]r', document.schema, {
+				const result = parse( '<$text bold="true">fo[o</$text>ba]r', document.schema, document.batch(), {
 					selectionAttributes: { bold: true }
 				} );
 
@@ -609,13 +609,13 @@ describe( 'model test utils', () => {
 
 			it( 'throws when missing selection start', () => {
 				expect( () => {
-					parse( 'foo]' );
+					parse( 'foo]', document.schema, document.batch() );
 				} ).to.throw( Error );
 			} );
 
 			it( 'throws when missing selection end', () => {
 				expect( () => {
-					parse( '[foo' );
+					parse( '[foo', document.schema, document.batch() );
 				} ).to.throw( Error );
 			} );
 		} );
@@ -623,7 +623,7 @@ describe( 'model test utils', () => {
 		function test( title, options ) {
 			it( title, () => {
 				const output = options.output || options.data;
-				const data = parse( options.data, document.schema );
+				const data = parse( options.data, document.schema, document.batch() );
 				let model, selection;
 
 				if ( data.selection && data.model ) {

+ 2 - 6
packages/ckeditor5-engine/tests/model/document/document.js

@@ -143,7 +143,7 @@ describe( 'Document', () => {
 			expect( changeCallback.args[ 0 ][ 4 ] ).to.equal( delta.type );
 		} );
 
-		it( 'should execute operation, fire event with proper data and not increase document version ' +
+		it( 'should execute operation, not fire event and not increase document version ' +
 			'when operation is not a document operation', () => {
 			const changeCallback = sinon.spy();
 			const type = 't';
@@ -169,11 +169,7 @@ describe( 'Document', () => {
 			expect( doc.history._deltas.length ).to.equal( 0 );
 			sinon.assert.calledOnce( operation._execute );
 
-			sinon.assert.calledOnce( changeCallback );
-			expect( changeCallback.args[ 0 ][ 1 ] ).to.equal( type );
-			expect( changeCallback.args[ 0 ][ 2 ] ).to.equal( data );
-			expect( changeCallback.args[ 0 ][ 3 ] ).to.deep.equal( batch );
-			expect( changeCallback.args[ 0 ][ 4 ] ).to.equal( delta.type );
+			sinon.assert.notCalled( changeCallback );
 		} );
 
 		it( 'should throw an error on the operation base version and the document version is different', () => {

+ 4 - 27
packages/ckeditor5-engine/tests/model/schema/schema.js

@@ -793,13 +793,6 @@ describe( 'Schema', () => {
 			} );
 
 			it( 'should filter out disallowed attributes from given nodes', () => {
-				schema.removeDisallowedAttributes( [ text, image ], '$root' );
-
-				expect( Array.from( text.getAttributeKeys() ) ).to.deep.equal( [ 'a' ] );
-				expect( Array.from( image.getAttributeKeys() ) ).to.deep.equal( [ 'b' ] );
-			} );
-
-			it( 'should filter out disallowed attributes from given nodes (batch)', () => {
 				const root = doc.getRoot();
 				const batch = doc.batch();
 
@@ -834,22 +827,6 @@ describe( 'Schema', () => {
 				div = new Element( 'div', [], [ paragraph, bar, imageInDiv ] );
 			} );
 
-			it( 'should filter out disallowed attributes from child nodes', () => {
-				schema.removeDisallowedAttributes( [ div ], '$root' );
-
-				expect( stringify( div ) )
-					.to.equal(
-						'<div>' +
-							'<paragraph>' +
-								'<$text b="1">foo</$text>' +
-								'<image b="1"></image>' +
-							'</paragraph>' +
-							'<$text a="1">bar</$text>' +
-							'<image a="1"></image>' +
-						'</div>'
-					);
-			} );
-
 			it( 'should filter out disallowed attributes from child nodes (batch)', () => {
 				const root = doc.getRoot();
 				const batch = doc.batch();
@@ -893,21 +870,21 @@ describe( 'Schema', () => {
 			} );
 
 			it( 'should accept iterable as nodes', () => {
-				schema.removeDisallowedAttributes( frag.getChildren(), '$root' );
+				schema.removeDisallowedAttributes( frag.getChildren(), '$root', doc.batch() );
 
 				expect( stringify( frag ) )
 					.to.equal( '<$text a="1">foo</$text><paragraph><$text b="1">bar</$text></paragraph>biz' );
 			} );
 
 			it( 'should accept Position as inside', () => {
-				schema.removeDisallowedAttributes( frag.getChildren(), Position.createAt( root ) );
+				schema.removeDisallowedAttributes( frag.getChildren(), Position.createAt( root ), doc.batch() );
 
 				expect( stringify( frag ) )
 					.to.equal( '<$text a="1">foo</$text><paragraph><$text b="1">bar</$text></paragraph>biz' );
 			} );
 
 			it( 'should accept Node as inside', () => {
-				schema.removeDisallowedAttributes( frag.getChildren(), [ root ] );
+				schema.removeDisallowedAttributes( frag.getChildren(), [ root ], doc.batch() );
 
 				expect( stringify( frag ) )
 					.to.equal( '<$text a="1">foo</$text><paragraph><$text b="1">bar</$text></paragraph>biz' );
@@ -920,7 +897,7 @@ describe( 'Schema', () => {
 
 			const image = new Element( 'image', { a: 1, b: 1 } );
 
-			schema.removeDisallowedAttributes( [ image ], '$root' );
+			schema.removeDisallowedAttributes( [ image ], '$root', doc.batch() );
 
 			expect( Array.from( image.getAttributeKeys() ) ).to.deep.equal( [ 'a', 'b' ] );
 		} );

+ 4 - 4
packages/ckeditor5-engine/tests/model/selection.js

@@ -894,14 +894,14 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return selected element', () => {
-			const { selection, model } = parse( '<p>foo</p>[<p>bar</p>]<p>baz</p>', schema );
+			const { selection, model } = parse( '<p>foo</p>[<p>bar</p>]<p>baz</p>', schema, doc.batch() );
 			const p = model.getChild( 1 );
 
 			expect( selection.getSelectedElement() ).to.equal( p );
 		} );
 
 		it( 'should return null if there is more than one range', () => {
-			const { selection } = parse( '[<p>foo</p>][<p>bar</p>]<p>baz</p>', schema );
+			const { selection } = parse( '[<p>foo</p>][<p>bar</p>]<p>baz</p>', schema, doc.batch() );
 
 			expect( selection.getSelectedElement() ).to.be.null;
 		} );
@@ -911,13 +911,13 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return null if selection is not over single element #1', () => {
-			const { selection } = parse( '<p>foo</p>[<p>bar</p><p>baz}</p>', schema );
+			const { selection } = parse( '<p>foo</p>[<p>bar</p><p>baz}</p>', schema, doc.batch() );
 
 			expect( selection.getSelectedElement() ).to.be.null;
 		} );
 
 		it( 'should return null if selection is not over single element #2', () => {
-			const { selection } = parse( '<p>{bar}</p>', schema );
+			const { selection } = parse( '<p>{bar}</p>', schema, doc.batch() );
 
 			expect( selection.getSelectedElement() ).to.be.null;
 		} );