Przeglądaj źródła

Merge branch 'master' into t/1198

Maciej Gołaszewski 8 lat temu
rodzic
commit
72246c6c3b
100 zmienionych plików z 3599 dodań i 4762 usunięć
  1. 21 30
      packages/ckeditor5-engine/src/controller/datacontroller.js
  2. 57 55
      packages/ckeditor5-engine/src/controller/deletecontent.js
  3. 7 7
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  4. 79 76
      packages/ckeditor5-engine/src/controller/getselectedcontent.js
  5. 49 53
      packages/ckeditor5-engine/src/controller/insertcontent.js
  6. 7 7
      packages/ckeditor5-engine/src/conversion/buildviewconverter.js
  7. 7 7
      packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js
  8. 5 5
      packages/ckeditor5-engine/src/conversion/view-selection-to-model-converters.js
  9. 1 1
      packages/ckeditor5-engine/src/conversion/view-to-model-converters.js
  10. 41 31
      packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
  11. 8 9
      packages/ckeditor5-engine/src/dev-utils/deltareplayer.js
  12. 13 12
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  13. 32 35
      packages/ckeditor5-engine/src/dev-utils/model.js
  14. 13 889
      packages/ckeditor5-engine/src/model/batch.js
  15. 1 1
      packages/ckeditor5-engine/src/model/delta/attributedelta.js
  16. 1 1
      packages/ckeditor5-engine/src/model/delta/insertdelta.js
  17. 2 2
      packages/ckeditor5-engine/src/model/delta/markerdelta.js
  18. 1 1
      packages/ckeditor5-engine/src/model/delta/mergedelta.js
  19. 1 1
      packages/ckeditor5-engine/src/model/delta/movedelta.js
  20. 1 1
      packages/ckeditor5-engine/src/model/delta/removedelta.js
  21. 1 1
      packages/ckeditor5-engine/src/model/delta/renamedelta.js
  22. 1 1
      packages/ckeditor5-engine/src/model/delta/rootattributedelta.js
  23. 1 1
      packages/ckeditor5-engine/src/model/delta/splitdelta.js
  24. 1 1
      packages/ckeditor5-engine/src/model/delta/unwrapdelta.js
  25. 1 1
      packages/ckeditor5-engine/src/model/delta/weakinsertdelta.js
  26. 1 1
      packages/ckeditor5-engine/src/model/delta/wrapdelta.js
  27. 45 103
      packages/ckeditor5-engine/src/model/document.js
  28. 4 4
      packages/ckeditor5-engine/src/model/documentfragment.js
  29. 38 27
      packages/ckeditor5-engine/src/model/documentselection.js
  30. 240 0
      packages/ckeditor5-engine/src/model/model.js
  31. 0 8
      packages/ckeditor5-engine/src/model/model.jsdoc
  32. 3 3
      packages/ckeditor5-engine/src/model/node.js
  33. 1 1
      packages/ckeditor5-engine/src/model/operation/markeroperation.js
  34. 1 1
      packages/ckeditor5-engine/src/model/operation/operation.js
  35. 1 1
      packages/ckeditor5-engine/src/model/operation/transform.js
  36. 6 6
      packages/ckeditor5-engine/src/model/schema.js
  37. 1 1
      packages/ckeditor5-engine/src/model/selection.js
  38. 1 1
      packages/ckeditor5-engine/src/model/text.js
  39. 1 1
      packages/ckeditor5-engine/src/model/textproxy.js
  40. 916 0
      packages/ckeditor5-engine/src/model/writer.js
  41. 3 1
      packages/ckeditor5-engine/src/view/domconverter.js
  42. 2 1
      packages/ckeditor5-engine/src/view/observer/focusobserver.js
  43. 122 98
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  44. 129 91
      packages/ckeditor5-engine/tests/controller/deletecontent.js
  45. 46 44
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  46. 101 79
      packages/ckeditor5-engine/tests/controller/getselectedcontent.js
  47. 217 217
      packages/ckeditor5-engine/tests/controller/insertcontent.js
  48. 109 108
      packages/ckeditor5-engine/tests/controller/modifyselection.js
  49. 29 24
      packages/ckeditor5-engine/tests/conversion/advanced-converters.js
  50. 9 10
      packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js
  51. 32 37
      packages/ckeditor5-engine/tests/conversion/buildviewconverter.js
  52. 32 31
      packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js
  53. 30 26
      packages/ckeditor5-engine/tests/conversion/model-to-view-converters.js
  54. 63 53
      packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js
  55. 5 5
      packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js
  56. 11 12
      packages/ckeditor5-engine/tests/conversion/view-to-model-converters.js
  57. 23 27
      packages/ckeditor5-engine/tests/conversion/viewconversiondispatcher.js
  58. 32 31
      packages/ckeditor5-engine/tests/dev-utils/deltareplayer.js
  59. 65 50
      packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js
  60. 58 57
      packages/ckeditor5-engine/tests/dev-utils/model.js
  61. 8 13
      packages/ckeditor5-engine/tests/manual/highlight.js
  62. 13 13
      packages/ckeditor5-engine/tests/manual/markers.js
  63. 3 4
      packages/ckeditor5-engine/tests/manual/nestededitable.js
  64. 1 1
      packages/ckeditor5-engine/tests/manual/tickets/1088/1.js
  65. 2 2
      packages/ckeditor5-engine/tests/manual/tickets/462/1.js
  66. 4 5
      packages/ckeditor5-engine/tests/manual/tickets/475/1.js
  67. 1 1
      packages/ckeditor5-engine/tests/manual/tickets/603/1.js
  68. 1 1
      packages/ckeditor5-engine/tests/manual/tickets/880/1.js
  69. 1 1
      packages/ckeditor5-engine/tests/model/_utils/utils.js
  70. 10 1874
      packages/ckeditor5-engine/tests/model/batch.js
  71. 5 4
      packages/ckeditor5-engine/tests/model/delta/attributedelta.js
  72. 4 3
      packages/ckeditor5-engine/tests/model/delta/delta.js
  73. 5 4
      packages/ckeditor5-engine/tests/model/delta/deltafactory.js
  74. 4 2
      packages/ckeditor5-engine/tests/model/delta/insertdelta.js
  75. 4 2
      packages/ckeditor5-engine/tests/model/delta/markerdelta.js
  76. 4 2
      packages/ckeditor5-engine/tests/model/delta/mergedelta.js
  77. 4 2
      packages/ckeditor5-engine/tests/model/delta/movedelta.js
  78. 12 11
      packages/ckeditor5-engine/tests/model/delta/renamedelta.js
  79. 4 2
      packages/ckeditor5-engine/tests/model/delta/splitdelta.js
  80. 4 3
      packages/ckeditor5-engine/tests/model/delta/transform/_utils/utils.js
  81. 4 2
      packages/ckeditor5-engine/tests/model/delta/transform/transform.js
  82. 4 2
      packages/ckeditor5-engine/tests/model/delta/unwrapdelta.js
  83. 4 2
      packages/ckeditor5-engine/tests/model/delta/wrapdelta.js
  84. 13 11
      packages/ckeditor5-engine/tests/model/document/change-event.js
  85. 85 146
      packages/ckeditor5-engine/tests/model/document/document.js
  86. 90 71
      packages/ckeditor5-engine/tests/model/documentselection.js
  87. 4 2
      packages/ckeditor5-engine/tests/model/liveposition.js
  88. 66 41
      packages/ckeditor5-engine/tests/model/liverange.js
  89. 13 10
      packages/ckeditor5-engine/tests/model/markercollection.js
  90. 346 0
      packages/ckeditor5-engine/tests/model/model.js
  91. 4 2
      packages/ckeditor5-engine/tests/model/node.js
  92. 24 23
      packages/ckeditor5-engine/tests/model/operation/attributeoperation.js
  93. 12 11
      packages/ckeditor5-engine/tests/model/operation/detachoperation.js
  94. 17 15
      packages/ckeditor5-engine/tests/model/operation/insertoperation.js
  95. 42 41
      packages/ckeditor5-engine/tests/model/operation/markeroperation.js
  96. 17 17
      packages/ckeditor5-engine/tests/model/operation/moveoperation.js
  97. 5 4
      packages/ckeditor5-engine/tests/model/operation/nooperation.js
  98. 4 4
      packages/ckeditor5-engine/tests/model/operation/operationfactory.js
  99. 9 7
      packages/ckeditor5-engine/tests/model/operation/reinsertoperation.js
  100. 13 12
      packages/ckeditor5-engine/tests/model/operation/removeoperation.js

+ 21 - 30
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -44,15 +44,15 @@ export default class DataController {
 	/**
 	 * Creates data controller instance.
 	 *
-	 * @param {module:engine/model/document~Document} model Document model.
+	 * @param {module:engine/model/model~Model} model Data model.
 	 * @param {module:engine/dataprocessor/dataprocessor~DataProcessor} [dataProcessor] Data processor which should used by the controller.
 	 */
 	constructor( model, dataProcessor ) {
 		/**
-		 * Document model.
+		 * Data model.
 		 *
 		 * @readonly
-		 * @member {module:engine/model/document~Document}
+		 * @member {module:engine/model/model~Model}
 		 */
 		this.model = model;
 
@@ -104,7 +104,7 @@ export default class DataController {
 		 * @readonly
 		 * @member {module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}
 		 */
-		this.viewToModel = new ViewConversionDispatcher( {
+		this.viewToModel = new ViewConversionDispatcher( this.model, {
 			schema: model.schema
 		} );
 
@@ -130,7 +130,7 @@ export default class DataController {
 	 */
 	get( rootName = 'main' ) {
 		// Get model range.
-		return this.stringify( this.model.getRoot( rootName ) );
+		return this.stringify( this.model.document.getRoot( rootName ) );
 	}
 
 	/**
@@ -186,19 +186,16 @@ export default class DataController {
 	 */
 	set( data, rootName = 'main' ) {
 		// Save to model.
-		const modelRoot = this.model.getRoot( rootName );
+		const modelRoot = this.model.document.getRoot( rootName );
 
-		this.model.enqueueChanges( () => {
+		this.model.enqueueChange( 'transparent', writer => {
 			// Clearing selection is a workaround for ticket #569 (LiveRange loses position after removing data from document).
 			// After fixing it this code should be removed.
-			this.model.selection.removeAllRanges();
-			this.model.selection.clearAttributes();
+			this.model.document.selection.removeAllRanges();
+			this.model.document.selection.clearAttributes();
 
-			// Initial batch should be ignored by features like undo, etc.
-			const batch = this.model.batch( 'transparent' );
-
-			batch.remove( ModelRange.createIn( modelRoot ) );
-			batch.insert( this.parse( data, batch ), modelRoot );
+			writer.remove( ModelRange.createIn( modelRoot ) );
+			writer.insert( this.parse( data ), modelRoot );
 		} );
 	}
 
@@ -208,17 +205,16 @@ 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, batch, context = '$root' ) {
+	parse( data, context = '$root' ) {
 		// data -> view
 		const viewDocumentFragment = this.processor.toView( data );
 
 		// view -> model
-		return this.toModel( viewDocumentFragment, batch, context );
+		return this.toModel( viewDocumentFragment, context );
 	}
 
 	/**
@@ -232,13 +228,12 @@ 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, batch, context = '$root' ) {
-		return this.viewToModel.convert( viewElementOrFragment, batch, { context: [ context ] } );
+	toModel( viewElementOrFragment, context = '$root' ) {
+		return this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } );
 	}
 
 	/**
@@ -252,11 +247,9 @@ export default class DataController {
 	 * @fires insertContent
 	 * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
 	 * @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
-	 * @param {module:engine/model/batch~Batch} [batch] Batch to which deltas will be added. If not specified, then
-	 * changes will be added to a new batch.
 	 */
-	insertContent( content, selection, batch ) {
-		insertContent( this, content, selection, batch );
+	insertContent( content, selection ) {
+		insertContent( this, content, selection );
 	}
 
 	/**
@@ -272,11 +265,10 @@ export default class DataController {
 	 *
 	 * @fires deleteContent
 	 * @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted.
-	 * @param {module:engine/model/batch~Batch} batch Batch to which deltas will be added.
 	 * @param {Object} options See {@link module:engine/controller/deletecontent~deleteContent}'s options.
 	 */
-	deleteContent( selection, batch, options ) {
-		deleteContent( selection, batch, options );
+	deleteContent( selection, options ) {
+		deleteContent( this, selection, options );
 	}
 
 	/**
@@ -295,11 +287,10 @@ export default class DataController {
 	 *
 	 * @fires module:engine/controller/datacontroller~DataController#getSelectedContent
 	 * @param {module:engine/model/selection~Selection} selection The selection of which content will be retrieved.
-	 * @param {module:engine/model/batch~Batch} batch Batch to which deltas will be added.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Document fragment holding the clone of the selected content.
 	 */
-	getSelectedContent( selection, batch ) {
-		return getSelectedContent( selection, batch );
+	getSelectedContent( selection ) {
+		return getSelectedContent( this, selection );
 	}
 
 	/**

+ 57 - 55
packages/ckeditor5-engine/src/controller/deletecontent.js

@@ -10,11 +10,12 @@
 import LivePosition from '../model/liveposition';
 import Position from '../model/position';
 import Range from '../model/range';
-import Element from '../model/element';
 
 /**
  * Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
  *
+ * @param {module:engine/controller/datacontroller~DataController} dataController The data controller in context of which the insertion
+ * should be performed.
  * @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted.
  * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
  * @param {Object} [options]
@@ -36,63 +37,65 @@ import Element from '../model/element';
  * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
  * * `<heading>^</heading>` with enabled (`doNotResetEntireContent == true`).
  */
-export default function deleteContent( selection, batch, options = {} ) {
+export default function deleteContent( dataController, selection, options = {} ) {
 	if ( selection.isCollapsed ) {
 		return;
 	}
 
-	const schema = batch.document.schema;
+	const schema = dataController.model.schema;
 
-	// 1. Replace the entire content with paragraph.
-	// See: https://github.com/ckeditor/ckeditor5-engine/issues/1012#issuecomment-315017594.
-	if ( !options.doNotResetEntireContent && shouldEntireContentBeReplacedWithParagraph( schema, selection ) ) {
-		replaceEntireContentWithParagraph( batch, selection );
+	dataController.model.change( writer => {
+		// 1. Replace the entire content with paragraph.
+		// See: https://github.com/ckeditor/ckeditor5-engine/issues/1012#issuecomment-315017594.
+		if ( !options.doNotResetEntireContent && shouldEntireContentBeReplacedWithParagraph( schema, selection ) ) {
+			replaceEntireContentWithParagraph( writer, selection, schema );
 
-		return;
-	}
+			return;
+		}
 
-	const selRange = selection.getFirstRange();
-	const startPos = selRange.start;
-	const endPos = LivePosition.createFromPosition( selRange.end );
+		const selRange = selection.getFirstRange();
+		const startPos = selRange.start;
+		const endPos = LivePosition.createFromPosition( selRange.end );
 
-	// 2. Remove the content if there is any.
-	if ( !selRange.start.isTouching( selRange.end ) ) {
-		batch.remove( selRange );
-	}
+		// 2. Remove the content if there is any.
+		if ( !selRange.start.isTouching( selRange.end ) ) {
+			writer.remove( selRange );
+		}
 
-	// 3. Merge elements in the right branch to the elements in the left branch.
-	// The only reasonable (in terms of data and selection correctness) case in which we need to do that is:
-	//
-	// <heading type=1>Fo[</heading><paragraph>]ar</paragraph> => <heading type=1>Fo^ar</heading>
-	//
-	// However, the algorithm supports also merging deeper structures (up to the depth of the shallower branch),
-	// as it's hard to imagine what should actually be the default behavior. Usually, specific features will
-	// want to override that behavior anyway.
-	if ( !options.leaveUnmerged ) {
-		mergeBranches( batch, startPos, endPos );
-
-		// We need to check and strip disallowed attributes in all nested nodes because after merge
-		// some attributes could end up in a path where are disallowed.
+		// 3. Merge elements in the right branch to the elements in the left branch.
+		// The only reasonable (in terms of data and selection correctness) case in which we need to do that is:
 		//
-		// e.g. bold is disallowed for <H1>
-		// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>.
-		schema.removeDisallowedAttributes( startPos.parent.getChildren(), startPos, batch );
-	}
+		// <heading type=1>Fo[</heading><paragraph>]ar</paragraph> => <heading type=1>Fo^ar</heading>
+		//
+		// However, the algorithm supports also merging deeper structures (up to the depth of the shallower branch),
+		// as it's hard to imagine what should actually be the default behavior. Usually, specific features will
+		// want to override that behavior anyway.
+		if ( !options.leaveUnmerged ) {
+			mergeBranches( writer, startPos, endPos );
+
+			// We need to check and strip disallowed attributes in all nested nodes because after merge
+			// some attributes could end up in a path where are disallowed.
+			//
+			// e.g. bold is disallowed for <H1>
+			// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>.
+			schema.removeDisallowedAttributes( startPos.parent.getChildren(), startPos, writer );
+		}
 
-	selection.setCollapsedAt( startPos );
+		selection.setCollapsedAt( startPos );
 
-	// 4. Autoparagraphing.
-	// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
-	if ( shouldAutoparagraph( schema, startPos ) ) {
-		insertParagraph( batch, startPos, selection );
-	}
+		// 4. Autoparagraphing.
+		// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
+		if ( shouldAutoparagraph( schema, startPos ) ) {
+			insertParagraph( writer, startPos, selection );
+		}
 
-	endPos.detach();
+		endPos.detach();
+	} );
 }
 
 // This function is a result of reaching the Ballmer's peak for just the right amount of time.
 // Even I had troubles documenting it after a while and after reading it again I couldn't believe that it really works.
-function mergeBranches( batch, startPos, endPos ) {
+function mergeBranches( writer, startPos, endPos ) {
 	const startParent = startPos.parent;
 	const endParent = endPos.parent;
 
@@ -112,7 +115,7 @@ function mergeBranches( batch, startPos, endPos ) {
 	// Check if operations we'll need to do won't need to cross object or limit boundaries.
 	// E.g., we can't merge endParent into startParent in this case:
 	// <limit><startParent>x[]</startParent></limit><endParent>{}</endParent>
-	if ( !checkCanBeMerged( startPos, endPos ) ) {
+	if ( !checkCanBeMerged( startPos, endPos, writer.model.schema ) ) {
 		return;
 	}
 
@@ -128,13 +131,13 @@ function mergeBranches( batch, startPos, endPos ) {
 		// <a><b>x[]</b></a><c><d>{}y</d></c>
 		// becomes:
 		// <a><b>x</b>[]<d>y</d></a><c>{}</c>
-		batch.insert( endParent, startPos );
+		writer.insert( endParent, startPos );
 	}
 
 	// Merge two siblings:
 	// <a>x</a>[]<b>y</b> -> <a>xy</a> (the usual case)
 	// <a><b>x</b>[]<d>y</d></a><c></c> -> <a><b>xy</b>[]</a><c></c> (this is the "move parent" case shown above)
-	batch.merge( startPos );
+	writer.merge( startPos );
 
 	// Remove empty end ancestors:
 	// <a>fo[o</a><b><a><c>bar]</c></a></b>
@@ -146,11 +149,11 @@ function mergeBranches( batch, startPos, endPos ) {
 
 		endPos = Position.createBefore( parentToRemove );
 
-		batch.remove( parentToRemove );
+		writer.remove( parentToRemove );
 	}
 
 	// Continue merging next level.
-	mergeBranches( batch, startPos, endPos );
+	mergeBranches( writer, startPos, endPos );
 }
 
 function shouldAutoparagraph( schema, position ) {
@@ -166,8 +169,7 @@ function shouldAutoparagraph( schema, position ) {
 // E.g. in <bQ><p>x[]</p></bQ><widget><caption>{}</caption></widget>
 // we'll check <p>, <bQ>, <widget> and <caption>.
 // Usually, widget and caption are marked as objects/limits in the schema, so in this case merging will be blocked.
-function checkCanBeMerged( leftPos, rightPos ) {
-	const schema = leftPos.root.document.schema;
+function checkCanBeMerged( leftPos, rightPos, schema ) {
 	const rangeToCheck = new Range( leftPos, rightPos );
 
 	for ( const value of rangeToCheck.getWalker() ) {
@@ -179,18 +181,18 @@ function checkCanBeMerged( leftPos, rightPos ) {
 	return true;
 }
 
-function insertParagraph( batch, position, selection ) {
-	const paragraph = new Element( 'paragraph' );
-	batch.insert( paragraph, position );
+function insertParagraph( writer, position, selection ) {
+	const paragraph = writer.createElement( 'paragraph' );
 
+	writer.insert( paragraph, position );
 	selection.setCollapsedAt( paragraph );
 }
 
-function replaceEntireContentWithParagraph( batch, selection ) {
-	const limitElement = batch.document.schema.getLimitElement( selection );
+function replaceEntireContentWithParagraph( writer, selection ) {
+	const limitElement = writer.model.schema.getLimitElement( selection );
 
-	batch.remove( Range.createIn( limitElement ) );
-	insertParagraph( batch, Position.createAt( limitElement ), selection );
+	writer.remove( Range.createIn( limitElement ) );
+	insertParagraph( writer, Position.createAt( limitElement ), selection );
 }
 
 // We want to replace the entire content with a paragraph when:

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

@@ -36,14 +36,14 @@ export default class EditingController {
 	/**
 	 * Creates editing controller instance.
 	 *
-	 * @param {module:engine/model/document~Document} model Document model.
+	 * @param {module:engine/model/model~Model} model Editing model.
 	 */
 	constructor( model ) {
 		/**
-		 * Document model.
+		 * Editing model.
 		 *
 		 * @readonly
-		 * @member {module:engine/model/document~Document}
+		 * @member {module:engine/model/model~Model}
 		 */
 		this.model = model;
 
@@ -84,13 +84,13 @@ export default class EditingController {
 		} );
 
 		// Convert changes in model to view.
-		this.listenTo( this.model, 'change', ( evt, type, changes ) => {
+		this.listenTo( this.model.document, 'change', ( evt, type, changes ) => {
 			this.modelToView.convertChange( type, changes );
 		}, { priority: 'low' } );
 
 		// Convert model selection to view.
-		this.listenTo( this.model, 'changesDone', () => {
-			const selection = this.model.selection;
+		this.listenTo( this.model.document, 'changesDone', () => {
+			const selection = this.model.document.selection;
 
 			this.modelToView.convertSelection( selection );
 			this.view.render();
@@ -139,7 +139,7 @@ export default class EditingController {
 	 */
 	createRoot( domRoot, name = 'main' ) {
 		const viewRoot = this.view.createRoot( domRoot, name );
-		const modelRoot = this.model.getRoot( name );
+		const modelRoot = this.model.document.getRoot( name );
 
 		this.mapper.bindElements( modelRoot, viewRoot );
 

+ 79 - 76
packages/ckeditor5-engine/src/controller/getselectedcontent.js

@@ -21,90 +21,93 @@ import Position from '../model/position';
  *
  *		<quote><h>st</h></quote><p>se</p>
  *
+ * @param {module:engine/controller/datacontroller~DataController} dataController The data controller in context of which
+ * the selection modification should be performed.
  * @param {module:engine/model/selection~Selection} selection The selection of which content will be returned.
- * @param {module:engine/model/batch~Batch} batch Batch to which deltas will be added.
  * @returns {module:engine/model/documentfragment~DocumentFragment}
  */
-export default function getSelectedContent( selection, batch ) {
-	const frag = batch.createDocumentFragment();
-	const range = selection.getFirstRange();
+export default function getSelectedContent( dataController, selection ) {
+	return dataController.model.change( writer => {
+		const frag = writer.createDocumentFragment();
+		const range = selection.getFirstRange();
 
-	if ( !range || range.isCollapsed ) {
-		return frag;
-	}
-
-	const root = range.start.root;
-	const commonPath = range.start.getCommonPath( range.end );
-	const commonParent = root.getNodeByPath( commonPath );
-
-	// ## 1st step
-	//
-	// First, we'll clone a fragment represented by a minimal flat range
-	// containing the original range to be cloned.
-	// E.g. let's consider such a range:
-	//
-	// <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
-	//
-	// A minimal flat range containing this one is:
-	//
-	// <p>x</p>[<quote><p>y</p><h>first</h></quote><p>second</p>]<p>z</p>
-	//
-	// We can easily clone this structure, preserving e.g. the <quote> element.
-	let flatSubtreeRange;
-
-	if ( range.start.parent == range.end.parent ) {
-		// The original range is flat, so take it.
-		flatSubtreeRange = range;
-	} else {
-		flatSubtreeRange = Range.createFromParentsAndOffsets(
-			commonParent, range.start.path[ commonPath.length ],
-			commonParent, range.end.path[ commonPath.length ] + 1
-		);
-	}
-
-	const howMany = flatSubtreeRange.end.offset - flatSubtreeRange.start.offset;
-
-	// Clone the whole contents.
-	for ( const item of flatSubtreeRange.getItems( { shallow: true } ) ) {
-		if ( item.is( 'textProxy' ) ) {
-			batch.appendText( item.data, item.getAttributes(), frag );
+		if ( !range || range.isCollapsed ) {
+			return frag;
+		}
+
+		const root = range.start.root;
+		const commonPath = range.start.getCommonPath( range.end );
+		const commonParent = root.getNodeByPath( commonPath );
+
+		// ## 1st step
+		//
+		// First, we'll clone a fragment represented by a minimal flat range
+		// containing the original range to be cloned.
+		// E.g. let's consider such a range:
+		//
+		// <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
+		//
+		// A minimal flat range containing this one is:
+		//
+		// <p>x</p>[<quote><p>y</p><h>first</h></quote><p>second</p>]<p>z</p>
+		//
+		// We can easily clone this structure, preserving e.g. the <quote> element.
+		let flatSubtreeRange;
+
+		if ( range.start.parent == range.end.parent ) {
+			// The original range is flat, so take it.
+			flatSubtreeRange = range;
 		} else {
-			batch.append( item.clone( true ), frag );
+			flatSubtreeRange = Range.createFromParentsAndOffsets(
+				commonParent, range.start.path[ commonPath.length ],
+				commonParent, range.end.path[ commonPath.length ] + 1
+			);
 		}
-	}
-
-	// ## 2nd step
-	//
-	// If the original range wasn't flat, then we need to remove the excess nodes from the both ends of the cloned fragment.
-	//
-	// For example, for the range shown in the 1st step comment, we need to remove these pieces:
-	//
-	// <quote>[<p>y</p>]<h>[fir]st</h></quote><p>se[cond]</p>
-	//
-	// So this will be the final copied content:
-	//
-	// <quote><h>st</h></quote><p>se</p>
-	//
-	// In order to do that, we remove content from these two ranges:
-	//
-	// [<quote><p>y</p><h>fir]st</h></quote><p>se[cond</p>]
-	if ( flatSubtreeRange != range ) {
-		// Find the position of the original range in the cloned fragment.
-		const newRange = range._getTransformedByMove( flatSubtreeRange.start, Position.createAt( frag, 0 ), howMany )[ 0 ];
-
-		const leftExcessRange = new Range( Position.createAt( frag ), newRange.start );
-		const rightExcessRange = new Range( newRange.end, Position.createAt( frag, 'end' ) );
-
-		removeRangeContent( rightExcessRange, batch );
-		removeRangeContent( leftExcessRange, batch );
-	}
-
-	return frag;
+
+		const howMany = flatSubtreeRange.end.offset - flatSubtreeRange.start.offset;
+
+		// Clone the whole contents.
+		for ( const item of flatSubtreeRange.getItems( { shallow: true } ) ) {
+			if ( item.is( 'textProxy' ) ) {
+				writer.appendText( item.data, item.getAttributes(), frag );
+			} else {
+				writer.append( item.clone( true ), frag );
+			}
+		}
+
+		// ## 2nd step
+		//
+		// If the original range wasn't flat, then we need to remove the excess nodes from the both ends of the cloned fragment.
+		//
+		// For example, for the range shown in the 1st step comment, we need to remove these pieces:
+		//
+		// <quote>[<p>y</p>]<h>[fir]st</h></quote><p>se[cond]</p>
+		//
+		// So this will be the final copied content:
+		//
+		// <quote><h>st</h></quote><p>se</p>
+		//
+		// In order to do that, we remove content from these two ranges:
+		//
+		// [<quote><p>y</p><h>fir]st</h></quote><p>se[cond</p>]
+		if ( flatSubtreeRange != range ) {
+			// Find the position of the original range in the cloned fragment.
+			const newRange = range._getTransformedByMove( flatSubtreeRange.start, Position.createAt( frag, 0 ), howMany )[ 0 ];
+
+			const leftExcessRange = new Range( Position.createAt( frag ), newRange.start );
+			const rightExcessRange = new Range( newRange.end, Position.createAt( frag, 'end' ) );
+
+			removeRangeContent( rightExcessRange, writer );
+			removeRangeContent( leftExcessRange, writer );
+		}
+
+		return frag;
+	} );
 }
 
 // After https://github.com/ckeditor/ckeditor5-engine/issues/690 is fixed,
 // this function will, most likely, be able to rewritten using getMinimalFlatRanges().
-function removeRangeContent( range, batch ) {
+function removeRangeContent( range, writer ) {
 	const parentsToCheck = [];
 
 	Array.from( range.getItems( { direction: 'backward' } ) )
@@ -126,7 +129,7 @@ function removeRangeContent( range, batch ) {
 		.forEach( itemRange => {
 			parentsToCheck.push( itemRange.start.parent );
 
-			batch.remove( itemRange );
+			writer.remove( itemRange );
 		} );
 
 	// Remove ancestors of the removed items if they turned to be empty now
@@ -139,7 +142,7 @@ function removeRangeContent( range, batch ) {
 
 			parent = parent.parent;
 
-			batch.remove( removeRange );
+			writer.remove( removeRange );
 		}
 	} );
 }

+ 49 - 53
packages/ckeditor5-engine/src/controller/insertcontent.js

@@ -26,51 +26,47 @@ import log from '@ckeditor/ckeditor5-utils/src/log';
  * should be performed.
  * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
  * @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
- * @param {module:engine/model/batch~Batch} [batch] Batch to which deltas will be added. If not specified, then
- * changes will be added to a new batch.
  */
-export default function insertContent( dataController, content, selection, batch ) {
-	if ( !batch ) {
-		batch = dataController.model.batch();
-	}
-
-	if ( !selection.isCollapsed ) {
-		dataController.deleteContent( selection, batch );
-	}
+export default function insertContent( dataController, content, selection ) {
+	dataController.model.change( writer => {
+		if ( !selection.isCollapsed ) {
+			dataController.deleteContent( selection );
+		}
 
-	const insertion = new Insertion( dataController, batch, selection.anchor );
+		const insertion = new Insertion( dataController, writer, selection.anchor );
 
-	let nodesToInsert;
+		let nodesToInsert;
 
-	if ( content.is( 'documentFragment' ) ) {
-		nodesToInsert = content.getChildren();
-	} else {
-		nodesToInsert = [ content ];
-	}
-
-	insertion.handleNodes( nodesToInsert, {
-		// The set of children being inserted is the only set in this context
-		// so it's the first and last (it's a hack ;)).
-		isFirst: true,
-		isLast: true
-	} );
+		if ( content.is( 'documentFragment' ) ) {
+			nodesToInsert = content.getChildren();
+		} else {
+			nodesToInsert = [ content ];
+		}
 
-	const newRange = insertion.getSelectionRange();
+		insertion.handleNodes( nodesToInsert, {
+			// The set of children being inserted is the only set in this context
+			// so it's the first and last (it's a hack ;)).
+			isFirst: true,
+			isLast: true
+		} );
 
-	/* istanbul ignore else */
-	if ( newRange ) {
-		selection.setRanges( [ newRange ] );
-	} else {
-		// We are not testing else because it's a safe check for unpredictable edge cases:
-		// an insertion without proper range to select.
+		const newRange = insertion.getSelectionRange();
 
-		/**
-		 * Cannot determine a proper selection range after insertion.
-		 *
-		 * @warning insertcontent-no-range
-		 */
-		log.warn( 'insertcontent-no-range: Cannot determine a proper selection range after insertion.' );
-	}
+		/* istanbul ignore else */
+		if ( newRange ) {
+			selection.setRanges( [ newRange ] );
+		} else {
+			// We are not testing else because it's a safe check for unpredictable edge cases:
+			// an insertion without proper range to select.
+
+			/**
+			 * Cannot determine a proper selection range after insertion.
+			 *
+			 * @warning insertcontent-no-range
+			 */
+			log.warn( 'insertcontent-no-range: Cannot determine a proper selection range after insertion.' );
+		}
+	} );
 }
 
 /**
@@ -79,7 +75,7 @@ export default function insertContent( dataController, content, selection, batch
  * @private
  */
 class Insertion {
-	constructor( dataController, batch, position ) {
+	constructor( dataController, writer, position ) {
 		/**
 		 * The data controller in context of which the insertion should be performed.
 		 *
@@ -90,9 +86,9 @@ class Insertion {
 		/**
 		 * Batch to which deltas will be added.
 		 *
-		 * @member {module:engine/controller/batch~Batch} #batch
+		 * @member {module:engine/controller/writer~Batch} #writer
 		 */
-		this.batch = batch;
+		this.writer = writer;
 
 		/**
 		 * The position at which (or near which) the next node will be inserted.
@@ -153,7 +149,7 @@ class Insertion {
 			return Range.createOn( this.nodeToSelect );
 		}
 
-		return this.dataController.model.getNearestSelectionRange( this.position );
+		return this.dataController.model.document.getNearestSelectionRange( this.position );
 	}
 
 	/**
@@ -229,7 +225,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.batch );
+			this.schema.removeDisallowedAttributes( [ node ], this.position, this.writer );
 			this._handleNode( node, context );
 		}
 		// If text is not allowed, try autoparagraphing.
@@ -256,7 +252,7 @@ class Insertion {
 
 		const livePos = LivePosition.createFromPosition( this.position );
 
-		this.batch.insert( node, this.position );
+		this.writer.insert( node, this.position );
 
 		this.position = Position.createFromPosition( livePos );
 		livePos.detach();
@@ -286,12 +282,12 @@ class Insertion {
 		if ( mergeLeft ) {
 			const position = LivePosition.createFromPosition( this.position );
 
-			this.batch.merge( mergePosLeft );
+			this.writer.merge( mergePosLeft );
 
 			// We need to check and strip disallowed attributes in all nested nodes because after merge
 			// some attributes could end up in a path where are disallowed.
 			const parent = position.nodeBefore;
-			this.schema.removeDisallowedAttributes( parent.getChildren(), Position.createAt( parent ), this.batch );
+			this.schema.removeDisallowedAttributes( parent.getChildren(), Position.createAt( parent ), this.writer );
 
 			this.position = Position.createFromPosition( position );
 			position.detach();
@@ -314,11 +310,11 @@ class Insertion {
 			// NOK: <p>xx[]</p> + <p>yy</p> => <p>xxyy[]</p> (when sticks to next)
 			const position = new LivePosition( this.position.root, this.position.path, 'sticksToPrevious' );
 
-			this.batch.merge( mergePosRight );
+			this.writer.merge( mergePosRight );
 
 			// We need to check and strip disallowed attributes in all nested nodes because after merge
 			// some attributes could end up in a place where are disallowed.
-			this.schema.removeDisallowedAttributes( position.parent.getChildren(), position, this.batch );
+			this.schema.removeDisallowedAttributes( position.parent.getChildren(), position, this.writer );
 
 			this.position = Position.createFromPosition( position );
 			position.detach();
@@ -330,7 +326,7 @@ class Insertion {
 		// When there was no merge we need to check and strip disallowed attributes in all nested nodes of
 		// just inserted node because some attributes could end up in a place where are disallowed.
 		if ( !mergeLeft && !mergeRight ) {
-			this.schema.removeDisallowedAttributes( node.getChildren(), Position.createAt( node ), this.batch );
+			this.schema.removeDisallowedAttributes( node.getChildren(), Position.createAt( node ), this.writer );
 		}
 	}
 
@@ -341,7 +337,7 @@ class Insertion {
 	 * @param {Object} context
 	 */
 	_tryAutoparagraphing( node, context ) {
-		const paragraph = this.batch.createElement( 'paragraph' );
+		const paragraph = this.writer.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 +346,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.batch );
+				this.schema.removeDisallowedAttributes( [ node ], [ paragraph ], this.writer );
 			}
 
 			if ( this._checkIsAllowed( node, [ paragraph ] ) ) {
@@ -385,14 +381,14 @@ class Insertion {
 				// Special case – parent is empty (<p>^</p>) so isAtStart == isAtEnd == true.
 				// We can remove the element after moving selection out of it.
 				if ( parent.isEmpty ) {
-					this.batch.remove( parent );
+					this.writer.remove( parent );
 				}
 			} else if ( this.position.isAtEnd ) {
 				this.position = Position.createAfter( this.position.parent );
 			} else {
 				const tempPos = Position.createAfter( this.position.parent );
 
-				this.batch.split( this.position );
+				this.writer.split( this.position );
 
 				this.position = tempPos;
 

+ 7 - 7
packages/ckeditor5-engine/src/conversion/buildviewconverter.js

@@ -263,14 +263,14 @@ class ViewConverterBuilder {
 	 *		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
 	 *		buildViewConverter().for( dispatcher )
 	 *			.fromElement( 'img' )
-	 *			.toElement( ( viewElement, batch ) => batch.createElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) );
+	 *			.toElement( ( viewElement, writer ) => writer.createElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) );
 	 *
 	 * @param {String|Function} element Model element name or model element creator function.
 	 */
 	toElement( element ) {
 		function eventCallbackGen( from ) {
 			return ( evt, data, consumable, conversionApi ) => {
-				const batch = conversionApi.batch;
+				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.
@@ -284,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, batch ) : batch.createElement( element );
+					const modelElement = element instanceof Function ? element( data.input, writer ) : writer.createElement( element );
 
 					// Do not convert if element building function returned falsy value.
 					if ( !modelElement ) {
@@ -311,7 +311,7 @@ class ViewConverterBuilder {
 					const modelChildren = conversionApi.convertChildren( data.input, consumable, data );
 
 					for ( const child of Array.from( modelChildren ) ) {
-						batch.append( child, modelElement );
+						writer.append( child, modelElement );
 					}
 
 					// Remove created `modelElement` from the parents stack.
@@ -435,7 +435,7 @@ class ViewConverterBuilder {
 	toMarker( creator ) {
 		function eventCallbackGen( from ) {
 			return ( evt, data, consumable, conversionApi ) => {
-				const batch = conversionApi.batch;
+				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.
@@ -453,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 = batch.createElement( '$marker', { 'data-name': data.input.getAttribute( 'data-name' ) } );
+					modelElement = writer.createElement( '$marker', { 'data-name': data.input.getAttribute( 'data-name' ) } );
 				}
 
 				// Check if model element is correct (has proper name and property).
@@ -528,7 +528,7 @@ function setAttributeOn( toChange, attribute, data, conversionApi ) {
 	};
 
 	if ( conversionApi.schema.check( schemaQuery ) ) {
-		conversionApi.batch.setAttribute( attribute.key, attribute.value, toChange );
+		conversionApi.writer.setAttribute( attribute.key, attribute.value, toChange );
 	}
 }
 

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

@@ -110,17 +110,17 @@ export default class ModelConversionDispatcher {
 	/**
 	 * Creates a `ModelConversionDispatcher` that operates using passed API.
 	 *
-	 * @param {module:engine/model/document~Document} modelDocument Model document instance bound with this dispatcher.
+	 * @param {module:engine/model/model~Model} model Data model.
 	 * @param {Object} [conversionApi] Interface passed by dispatcher to the events callbacks.
 	 */
-	constructor( modelDocument, conversionApi = {} ) {
+	constructor( model, conversionApi = {} ) {
 		/**
-		 * Model document instance bound with this dispatcher.
+		 * Data model instance bound with this dispatcher.
 		 *
 		 * @private
-		 * @member {module:engine/model/document~Document}
+		 * @member {module:engine/model/model~Model}
 		 */
-		this._modelDocument = modelDocument;
+		this._model = model;
 
 		/**
 		 * Interface passed by dispatcher to the events callbacks.
@@ -215,7 +215,7 @@ export default class ModelConversionDispatcher {
 			}
 		}
 
-		for ( const marker of this._modelDocument.markers ) {
+		for ( const marker of this._model.markers ) {
 			const markerRange = marker.getRange();
 			const intersection = markerRange.getIntersection( range );
 
@@ -357,7 +357,7 @@ export default class ModelConversionDispatcher {
 	 * @param {module:engine/model/selection~Selection} selection Selection to convert.
 	 */
 	convertSelection( selection ) {
-		const markers = Array.from( this._modelDocument.markers.getMarkersAtPosition( selection.getFirstPosition() ) );
+		const markers = Array.from( this._model.markers.getMarkersAtPosition( selection.getFirstPosition() ) );
 		const consumable = this._createSelectionConsumable( selection, markers );
 
 		this.fire( 'selection', { selection }, consumable, this.conversionApi );

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

@@ -22,11 +22,11 @@ import ModelSelection from '../model/selection';
  *
  *		view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) );
  *
- * @param {module:engine/model/document~Document} modelDocument Model document on which selection should be updated.
+ * @param {module:engine/model/model~Model} model Data model.
  * @param {module:engine/conversion/mapper~Mapper} mapper Conversion mapper.
  * @returns {Function} {@link module:engine/view/document~Document#event:selectionChange} callback function.
  */
-export function convertSelectionChange( modelDocument, mapper ) {
+export function convertSelectionChange( model, mapper ) {
 	return ( evt, data ) => {
 		const viewSelection = data.newSelection;
 		const modelSelection = new ModelSelection();
@@ -39,9 +39,9 @@ export function convertSelectionChange( modelDocument, mapper ) {
 
 		modelSelection.setRanges( ranges, viewSelection.isBackward );
 
-		if ( !modelSelection.isEqual( modelDocument.selection ) ) {
-			modelDocument.enqueueChanges( () => {
-				modelDocument.selection.setTo( modelSelection );
+		if ( !modelSelection.isEqual( model.document.selection ) ) {
+			model.change( () => {
+				model.document.selection.setTo( modelSelection );
 			} );
 		}
 	};

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

@@ -48,7 +48,7 @@ export function convertText() {
 
 		if ( conversionApi.schema.check( schemaQuery ) ) {
 			if ( consumable.consume( data.input ) ) {
-				data.output = conversionApi.batch.createText( data.input.data );
+				data.output = conversionApi.writer.createText( data.input.data );
 			}
 		}
 	};

+ 41 - 31
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -91,7 +91,7 @@ import log from '@ckeditor/ckeditor5-utils/src/log';
  *		// Fire conversion.
  *		// Always take care where the converted model structure will be appended to. If this `viewDocumentFragment`
  *		// is going to be appended directly to a '$root' element, use that in `context`.
- *		viewDispatcher.convert( viewDocumentFragment, batch, { context: [ '$root' ] } );
+ *		viewDispatcher.convert( viewDocumentFragment, { context: [ '$root' ] } );
  *
  * Before each conversion process, `ViewConversionDispatcher` fires {@link ~ViewConversionDispatcher#event:viewCleanup}
  * event which can be used to prepare tree view for conversion.
@@ -107,10 +107,19 @@ export default class ViewConversionDispatcher {
 	 * Creates a `ViewConversionDispatcher` that operates using passed API.
 	 *
 	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi
+	 * @param {module:engine/model/model~Model} model Data model.
 	 * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
 	 * by `ViewConversionDispatcher`.
 	 */
-	constructor( conversionApi = {} ) {
+	constructor( model, conversionApi = {} ) {
+		/**
+		 * Data model.
+		 *
+		 * @private
+		 * @type {module:engine/model/model~Model}
+		 */
+		this._model = model;
+
 		/**
 		 * Interface passed by dispatcher to the events callbacks.
 		 *
@@ -122,9 +131,6 @@ export default class ViewConversionDispatcher {
 		// 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 );
-
-		// Batch used for conversion. Is passed to #convert method and removed at the and of the conversion.
-		this.conversionApi.batch = null;
 	}
 
 	/**
@@ -135,40 +141,44 @@ 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 {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
 	 * @param {Object} additionalData Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
 	 * events. See also {@link ~ViewConversionDispatcher#event:element element event}.
 	 * @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, batch, additionalData ) {
-		// Store batch in current conversion as conversionApi, will be removed at the end of this conversion.
-		this.conversionApi.batch = batch;
+	convert( viewItem, additionalData ) {
+		return this._model.change( writer => {
+			// Store writer in current conversion as a conversion API.
+			this.conversionApi.writer = writer;
 
-		this.fire( 'viewCleanup', viewItem );
+			this.fire( 'viewCleanup', viewItem );
 
-		const consumable = ViewConsumable.createFrom( viewItem );
-		let conversionResult = this._convertItem( viewItem, consumable, additionalData );
+			const consumable = ViewConsumable.createFrom( viewItem );
+			let conversionResult = this._convertItem( viewItem, consumable, additionalData );
 
-		// 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 batch.createDocumentFragment();
-		}
+			// Remove writer from conversion API after conversion.
+			this.conversionApi.writer = null;
 
-		// When conversion result is not a document fragment we need to wrap it in document fragment.
-		if ( !conversionResult.is( 'documentFragment' ) ) {
-			const docFrag = batch.createDocumentFragment();
+			// 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();
+			}
 
-			batch.append( conversionResult, docFrag );
-			conversionResult = docFrag;
-		}
+			// When conversion result is not a document fragment we need to wrap it in document fragment.
+			if ( !conversionResult.is( 'documentFragment' ) ) {
+				const docFrag = writer.createDocumentFragment();
 
-		// Extract temporary markers elements from model and set as static markers collection.
-		conversionResult.markers = extractMarkersFromModelFragment( conversionResult, batch );
+				writer.append( conversionResult, docFrag );
+				conversionResult = docFrag;
+			}
 
-		return conversionResult;
+			// Extract temporary markers elements from model and set as static markers collection.
+			conversionResult.markers = extractMarkersFromModelFragment( conversionResult, writer );
+
+			return conversionResult;
+		} );
 	}
 
 	/**
@@ -212,14 +222,14 @@ export default class ViewConversionDispatcher {
 	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren
 	 */
 	_convertChildren( input, consumable, additionalData ) {
-		const batch = this.conversionApi.batch;
-		const documentFragment = batch.createDocumentFragment();
+		const writer = this.conversionApi.writer;
+		const documentFragment = writer.createDocumentFragment();
 
 		for ( const viewChild of Array.from( input.getChildren() ) ) {
 			const modelChild = this._convertItem( viewChild, consumable, additionalData );
 
 			if ( modelChild instanceof ModelNode || modelChild instanceof ModelDocumentFragment ) {
-				batch.append( modelChild, documentFragment );
+				writer.append( modelChild, documentFragment );
 			}
 		}
 
@@ -281,7 +291,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, batch ) {
+function extractMarkersFromModelFragment( modelItem, writer ) {
 	const markerElements = new Set();
 	const markers = new Map();
 
@@ -313,7 +323,7 @@ function extractMarkersFromModelFragment( modelItem, batch ) {
 		}
 
 		// Remove marker element from DocumentFragment.
-		batch.remove( markerElement );
+		writer.remove( markerElement );
 	}
 
 	return markers;

+ 8 - 9
packages/ckeditor5-engine/src/dev-utils/deltareplayer.js

@@ -16,12 +16,12 @@ import DeltaFactory from '../model/delta/deltafactory';
  */
 export default class DeltaReplayer {
 	/**
-	 * @param {module:engine/model/document~Document} document Document to replay deltas on.
+	 * @param {module:engine/model/model~Model} model Data model.
 	 * @param {String} logSeparator Separator between deltas.
 	 * @param {String} stringifiedDeltas Deltas to replay.
 	 */
-	constructor( document, logSeparator, stringifiedDeltas ) {
-		this._document = document;
+	constructor( model, logSeparator, stringifiedDeltas ) {
+		this._model = model;
 		this._logSeparator = logSeparator;
 		this.setStringifiedDeltas( stringifiedDeltas );
 	}
@@ -118,23 +118,22 @@ export default class DeltaReplayer {
 	 * @returns {Promise.<Boolean>}
 	 */
 	applyNextDelta() {
-		const document = this._document;
+		const model = this._model;
 
 		return new Promise( res => {
-			document.enqueueChanges( () => {
+			model.enqueueChange( writer => {
 				const jsonDelta = this._deltasToReplay.shift();
 
 				if ( !jsonDelta ) {
 					return res( true );
 				}
 
-				const delta = DeltaFactory.fromJSON( jsonDelta, this._document );
+				const delta = DeltaFactory.fromJSON( jsonDelta, model.document );
 
-				const batch = document.batch();
-				batch.addDelta( delta );
+				writer.batch.addDelta( delta );
 
 				for ( const operation of delta.operations ) {
-					document.applyOperation( operation );
+					model.applyOperation( operation );
 				}
 
 				res( false );

+ 13 - 12
packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js

@@ -37,6 +37,7 @@ import SplitDelta from '../model/delta/splitdelta';
 import UnwrapDelta from '../model/delta/unwrapdelta';
 import WrapDelta from '../model/delta/wrapdelta';
 import deltaTransform from '../model/delta/transform';
+import Model from '../model/model';
 import ModelDocument from '../model/document';
 import ModelDocumentFragment from '../model/documentfragment';
 import ModelRootElement from '../model/rootelement';
@@ -565,9 +566,9 @@ function enableLoggingTools() {
 }
 
 function enableReplayerTools() {
-	const _modelDocumentApplyOperation = ModelDocument.prototype.applyOperation;
+	const _modelApplyOperation = Model.prototype.applyOperation;
 
-	sandbox.mock( ModelDocument.prototype, 'applyOperation', function( operation ) {
+	sandbox.mock( Model.prototype, 'applyOperation', function( operation ) {
 		if ( !this._lastDelta ) {
 			this._appliedDeltas = [];
 		} else if ( this._lastDelta !== operation.delta ) {
@@ -576,10 +577,10 @@ function enableReplayerTools() {
 
 		this._lastDelta = operation.delta;
 
-		_modelDocumentApplyOperation.call( this, operation );
+		return _modelApplyOperation.call( this, operation );
 	} );
 
-	sandbox.mock( ModelDocument.prototype, 'getAppliedDeltas', function() {
+	sandbox.mock( Model.prototype, 'getAppliedDeltas', function() {
 		// No deltas has been applied yet, return empty string.
 		if ( !this._lastDelta ) {
 			return '';
@@ -590,15 +591,15 @@ function enableReplayerTools() {
 		return appliedDeltas.map( JSON.stringify ).join( LOG_SEPARATOR );
 	} );
 
-	sandbox.mock( ModelDocument.prototype, 'createReplayer', function( stringifiedDeltas ) {
+	sandbox.mock( Model.prototype, 'createReplayer', function( stringifiedDeltas ) {
 		return new DeltaReplayer( this, LOG_SEPARATOR, stringifiedDeltas );
 	} );
 }
 
 function enableDocumentTools() {
-	const _modelDocumentApplyOperation = ModelDocument.prototype.applyOperation;
+	const _modelApplyOperation = Model.prototype.applyOperation;
 
-	sandbox.mock( ModelDocument.prototype, 'applyOperation', function( operation ) {
+	sandbox.mock( Model.prototype, 'applyOperation', function( operation ) {
 		logger.log( 'Applying ' + operation );
 
 		if ( !this._operationLogs ) {
@@ -607,7 +608,7 @@ function enableDocumentTools() {
 
 		this._operationLogs.push( JSON.stringify( operation.toJSON() ) );
 
-		_modelDocumentApplyOperation.call( this, operation );
+		return _modelApplyOperation.call( this, operation );
 	} );
 
 	sandbox.mock( ModelDocument.prototype, 'log', function( version = null ) {
@@ -621,9 +622,9 @@ function enableDocumentTools() {
 	} );
 
 	sandbox.mock( Editor.prototype, 'logModel', function( version = null ) {
-		version = version === null ? this.document.version : version;
+		version = version === null ? this.model.document.version : version;
 
-		this.document.log( version );
+		this.model.document.log( version );
 	} );
 
 	sandbox.mock( Editor.prototype, 'logView', function( version ) {
@@ -631,7 +632,7 @@ function enableDocumentTools() {
 	} );
 
 	sandbox.mock( Editor.prototype, 'logDocuments', function( version = null ) {
-		version = version === null ? this.document.version : version;
+		version = version === null ? this.model.document.version : version;
 
 		this.logModel( version );
 		this.logView( version );
@@ -655,7 +656,7 @@ class DebugPlugin extends Plugin {
 	constructor( editor ) {
 		super( editor );
 
-		const modelDocument = this.editor.document;
+		const modelDocument = this.editor.model.document;
 		const viewDocument = this.editor.editing.view;
 
 		modelDocument[ treeDump ] = [];

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

@@ -12,14 +12,13 @@
  */
 
 import RootElement from '../model/rootelement';
-import ModelDocument from '../model/document';
+import Model from '../model/model';
+import Batch from '../model/batch';
 import ModelRange from '../model/range';
 import ModelPosition from '../model/position';
 import ModelConversionDispatcher from '../conversion/modelconversiondispatcher';
 import ModelSelection from '../model/selection';
 import ModelDocumentFragment from '../model/documentfragment';
-import ModelElement from '../model/element';
-import ModelText from '../model/text';
 
 import ViewConversionDispatcher from '../conversion/viewconversiondispatcher';
 import ViewSelection from '../view/selection';
@@ -44,7 +43,7 @@ import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObjec
  *
  *		<$text attribute="value">Text data</$text>
  *
- * @param {module:engine/model/document~Document} document
+ * @param {module:engine/model/model~Model} model
  * @param {Object} [options]
  * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will
  * be not included in returned string.
@@ -52,16 +51,16 @@ import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObjec
  * default `main` name will be used.
  * @returns {String} The stringified data.
  */
-export function getData( document, options = {} ) {
-	if ( !( document instanceof ModelDocument ) ) {
-		throw new TypeError( 'Document needs to be an instance of module:engine/model/document~Document.' );
+export function getData( model, options = {} ) {
+	if ( !( model instanceof Model ) ) {
+		throw new TypeError( 'Model needs to be an instance of module:engine/model/model~Model.' );
 	}
 
 	const withoutSelection = !!options.withoutSelection;
 	const rootName = options.rootName || 'main';
-	const root = document.getRoot( rootName );
+	const root = model.document.getRoot( rootName );
 
-	return withoutSelection ? getData._stringify( root ) : getData._stringify( root, document.selection );
+	return withoutSelection ? getData._stringify( root ) : getData._stringify( root, model.document.selection );
 }
 
 // Set stringify as getData private method - needed for testing/spying.
@@ -69,15 +68,14 @@ getData._stringify = stringify;
 
 /**
  * Sets the contents of the {@link module:engine/model/document~Document Document} provided as HTML-like string.
- * It uses {@link module:engine/model/document~Document#enqueueChanges enqueueChanges} method.
  *
- * **Note:** Remember to register elements in {@link module:engine/model/document~Document#schema document's schema} before inserting them.
+ * **Note:** Remember to register elements in {@link module:engine/model/model~Model#schema model's schema} before inserting them.
  *
  * **Note:** To create {@link module:engine/model/text~Text text} node witch containing attributes use:
  *
  *		<$text attribute="value">Text data</$text>
  *
- * @param {module:engine/model/document~Document} document
+ * @param {module:engine/model/model~Model} model
  * @param {String} data HTML-like string to write into Document.
  * @param {Object} options
  * @param {String} [options.rootName='main'] Root name where parsed data will be stored. If not provided, default `main`
@@ -87,18 +85,17 @@ getData._stringify = stringify;
  * @param {String} [options.batchType='transparent'] Batch type used for inserting elements.
  * See {@link module:engine/model/batch~Batch#type}.
  */
-export function setData( document, data, options = {} ) {
-	if ( !( document instanceof ModelDocument ) ) {
-		throw new TypeError( 'Document needs to be an instance of module:engine/model/document~Document.' );
+export function setData( model, data, options = {} ) {
+	if ( !( model instanceof Model ) ) {
+		throw new TypeError( 'Model needs to be an instance of module:engine/model/model~Model.' );
 	}
 
 	let modelDocumentFragment, selection;
-	const modelRoot = document.getRoot( options.rootName || 'main' );
-
-	const batch = document.batch( options.batchType || 'transparent' );
+	const modelRoot = model.document.getRoot( options.rootName || 'main' );
+	const batch = new Batch( options.batchType || 'transparent' );
 
 	// Parse data string to model.
-	const parsedResult = setData._parse( data, document.schema, batch, {
+	const parsedResult = setData._parse( data, model.schema, {
 		lastRangeBackward: options.lastRangeBackward,
 		selectionAttributes: options.selectionAttributes,
 		context: [ modelRoot.name ]
@@ -112,14 +109,14 @@ export function setData( document, data, options = {} ) {
 		modelDocumentFragment = parsedResult;
 	}
 
-	document.enqueueChanges( () => {
+	model.enqueueChange( batch, writer => {
 		// Replace existing model in document by new one.
-		batch.remove( ModelRange.createIn( modelRoot ) );
-		batch.insert( modelDocumentFragment, modelRoot );
+		writer.remove( ModelRange.createIn( modelRoot ) );
+		writer.insert( modelDocumentFragment, modelRoot );
 
 		// Clean up previous document selection.
-		document.selection.clearAttributes();
-		document.selection.removeAllRanges();
+		model.document.selection.clearAttributes();
+		model.document.selection.removeAllRanges();
 
 		// Update document selection if specified.
 		if ( selection ) {
@@ -132,10 +129,10 @@ export function setData( document, data, options = {} ) {
 				ranges.push( new ModelRange( start, end ) );
 			}
 
-			document.selection.setRanges( ranges, selection.isBackward );
+			model.document.selection.setRanges( ranges, selection.isBackward );
 
 			if ( options.selectionAttributes ) {
-				document.selection.setAttributesTo( selection.getAttributes() );
+				model.document.selection.setAttributesTo( selection.getAttributes() );
 			}
 		}
 	} );
@@ -161,7 +158,7 @@ setData._parse = parse;
  * @returns {String} HTML-like string representing the model.
  */
 export function stringify( node, selectionOrPositionOrRange = null ) {
-	const modelDoc = new ModelDocument();
+	const model = new Model();
 	const mapper = new Mapper();
 	let selection, range;
 
@@ -195,7 +192,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	// Setup model to view converter.
 	const viewDocumentFragment = new ViewDocumentFragment();
 	const viewSelection = new ViewSelection();
-	const modelToView = new ModelConversionDispatcher( modelDoc, { mapper, viewSelection } );
+	const modelToView = new ModelConversionDispatcher( model, { mapper, viewSelection } );
 
 	// Bind root elements.
 	mapper.bindElements( node.root, viewDocumentFragment );
@@ -252,7 +249,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, batch, options = {} ) {
+export function parse( data, schema, options = {} ) {
 	const mapper = new Mapper();
 
 	// Replace not accepted by XML `$text` tag name by valid one `model-text-with-attributes`.
@@ -275,7 +272,7 @@ export function parse( data, schema, batch, options = {} ) {
 	}
 
 	// Setup view to model converter.
-	const viewToModel = new ViewConversionDispatcher( { schema, mapper } );
+	const viewToModel = new ViewConversionDispatcher( new Model(), { schema, mapper } );
 
 	viewToModel.on( 'documentFragment', convertToModelFragment() );
 	viewToModel.on( 'element:model-text-with-attributes', convertToModelText( true ) );
@@ -283,10 +280,10 @@ export function parse( data, schema, batch, options = {} ) {
 	viewToModel.on( 'text', convertToModelText() );
 
 	// Convert view to model.
-	let model = viewToModel.convert( viewDocumentFragment.root, batch, { context: options.context || [ '$root' ] } );
+	let model = viewToModel.convert( viewDocumentFragment.root, { context: options.context || [ '$root' ] } );
 
 	// If root DocumentFragment contains only one element - return that element.
-	if ( model.is( 'documentFragment' ) && model.childCount == 1 ) {
+	if ( model.childCount == 1 ) {
 		model = model.getChild( 0 );
 	}
 
@@ -346,7 +343,7 @@ function convertToModelElement() {
 		// E.g. `bold="true"` - value will be parsed from string `"true"` to boolean `true`.
 		const attributes = convertAttributes( data.input.getAttributes(), parseAttributeValue );
 
-		data.output = new ModelElement( data.input.name, attributes );
+		data.output = conversionApi.writer.createElement( data.input.name, attributes );
 		conversionApi.mapper.bindElements( data.output, data.input );
 
 		data.context.push( data.output );
@@ -375,9 +372,9 @@ function convertToModelText( withAttributes = false ) {
 			// E.g. `bold="true"` - value will be parsed from string `"true"` to boolean `true`.
 			const attributes = convertAttributes( data.input.getAttributes(), parseAttributeValue );
 
-			node = new ModelText( data.input.getChild( 0 ).data, attributes );
+			node = conversionApi.writer.createText( data.input.getChild( 0 ).data, attributes );
 		} else {
-			node = new ModelText( data.input.data );
+			node = conversionApi.writer.createText( data.input.data );
 		}
 
 		data.output = node;

+ 13 - 889
packages/ckeditor5-engine/src/model/batch.js

@@ -7,77 +7,32 @@
  * @module engine/model/batch
  */
 
-import AttributeDelta from './delta/attributedelta';
-import InsertDelta from './delta/insertdelta';
-import MarkerDelta from './delta/markerdelta';
-import MergeDelta from './delta/mergedelta';
-import MoveDelta from './delta/movedelta';
-import RemoveDelta from './delta/removedelta';
-import RenameDelta from './delta/renamedelta';
-import RootAttributeDelta from './delta/rootattributedelta';
-import SplitDelta from './delta/splitdelta';
-import UnwrapDelta from './delta/unwrapdelta';
-import WeakInsertDelta from './delta/weakinsertdelta';
-import WrapDelta from './delta/wrapdelta';
-
-import AttributeOperation from './operation/attributeoperation';
-import DetachOperation from './operation/detachoperation';
-import InsertOperation from './operation/insertoperation';
-import MarkerOperation from './operation/markeroperation';
-import MoveOperation from './operation/moveoperation';
-import RemoveOperation from './operation/removeoperation';
-import RenameOperation from './operation/renameoperation';
-import RootAttributeOperation from './operation/rootattributeoperation';
-
-import DocumentFragment from './documentfragment';
-import Text from './text';
-import Element from './element';
-import RootElement from './rootelement';
-import Position from './position';
-import Range from './range.js';
-
-import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
-
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
 /**
- * `Batch` instance groups document changes ({@link module:engine/model/delta/delta~Delta deltas}). All deltas grouped in a single `Batch`
+ * `Batch` instance groups model changes ({@link module:engine/model/delta/delta~Delta deltas}). All deltas grouped in a single `Batch`
  * can be reverted together, so you can think about `Batch` as of a single undo step. If you want to extend given undo step you
- * can call another method on the same `Batch` object. If you want to create a separate undo step you can create a new `Batch`.
+ * can add more changes to the batch using {@link module:engine/model/model~Model#enqueueChange}:
  *
- * For example to create two separate undo steps you can call:
- *
- *		doc.batch().insert( 'foo', firstPosition );
- *		doc.batch().insert( 'bar', secondPosition );
- *
- * To create a single undo step:
- *
- *		const batch = doc.batch();
- *		batch.insert( 'foo', firstPosition );
- *		batch.insert( 'bar', secondPosition );
+ *		model.enqueueChange( batch, writer => {
+ *			writer.insertText( 'foo', paragraph, 'end' );
+ *		} );
  *
+ * @see module:engine/model/model~Model#enqueueChange
+ * @see module:engine/model/model~Model#change
  */
 export default class Batch {
 	/**
-	 * Creates `Batch` instance. Not recommended to use directly, use {@link module:engine/model/document~Document#batch} instead.
+	 * Creates `Batch` instance.
 	 *
-	 * @param {module:engine/model/document~Document} document Document which this Batch changes.
+	 * @see module:engine/model/model~Model#enqueueChange
+	 * @see module:engine/model/model~Model#change
 	 * @param {'transparent'|'default'} [type='default'] Type of the batch.
 	 */
-	constructor( document, type = 'default' ) {
-		/**
-		 * Document which this batch changes.
-		 *
-		 * @readonly
-		 * @member {module:engine/model/document~Document} module:engine/model/batch~Batch#document
-		 */
-		this.document = document;
-
+	constructor( type = 'default' ) {
 		/**
 		 * Array of deltas which compose this batch.
 		 *
 		 * @readonly
-		 * @member {Array.<module:engine/model/delta/delta~Delta>} module:engine/model/batch~Batch#deltas
+		 * @type {Array.<module:engine/model/delta/delta~Delta>}
 		 */
 		this.deltas = [];
 
@@ -89,7 +44,7 @@ export default class Batch {
 		 * * `'transparent'` - batch that should be ignored by other features, i.e. initial batch or collaborative editing changes.
 		 *
 		 * @readonly
-		 * @member {'transparent'|'default'} module:engine/model/batch~Batch#type
+		 * @type {'transparent'|'default'}
 		 */
 		this.type = type;
 	}
@@ -129,835 +84,4 @@ export default class Batch {
 			yield* delta.operations;
 		}
 	}
-
-	/**
-	 * Creates a new {@link module:engine/model/text~Text text node}.
-	 *
-	 *		batch.createText( 'foo' );
-	 *		batch.createText( 'foo', { 'bold': true } );
-	 *
-	 * @param {String} data Text data.
-	 * @param {Object} [attributes] Text attributes.
-	 * @returns {module:engine/model/text~Text} Created text node.
-	 */
-	createText( data, attributes ) {
-		return new Text( data, attributes );
-	}
-
-	/**
-	 * Creates a new {@link module:engine/model/element~Element element}.
-	 *
-	 *		batch.createElement( 'paragraph' );
-	 *		batch.createElement( 'paragraph', { 'alignment': 'center' } );
-	 *
-	 * @param {String} name Name of the element.
-	 * @param {Object} [attributes] Elements attributes.
-	 * @returns {module:engine/model/element~Element} Created element.
-	 */
-	createElement( name, attributes ) {
-		return new Element( name, attributes );
-	}
-
-	/**
-	 * Creates a new {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
-	 *
-	 * @returns {module:engine/model/documentfragment~DocumentFragment} Created document fragment.
-	 */
-	createDocumentFragment() {
-		return new DocumentFragment();
-	}
-
-	/**
-	 * Inserts item on given position.
-	 *
-	 *		const paragraph = batch.createElement( 'paragraph' );
-	 *		batch.insert( paragraph, position );
-	 *
-	 * Instead of using position you can use parent and offset:
-	 *
-	 * 		const text = batch.createText( 'foo' );
-	 *		batch.insert( text, paragraph, 5 );
-	 *
-	 * You can also use `end` instead of the offset to insert at the end:
-	 *
-	 * 		const text = batch.createText( 'foo' );
-	 *		batch.insert( text, paragraph, 'end' );
-	 *
-	 * Or insert before or after another element:
-	 *
-	 * 		const paragraph = batch.createElement( 'paragraph' );
-	 *		batch.insert( paragraph, anotherParagraph, 'after' );
-	 *
-	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
-	 *
-	 * Note that if the item already has parent it will be removed from the previous parent.
-	 *
-	 * If you want to move {@link module:engine/model/range~Range range} instead of an
-	 * {@link module:engine/model/item~Item item} use {@link module:engine/model/batch~Batch#move move}.
-	 *
-	 * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
-	 * item Item or document fragment to insert.
-	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
-	 * second parameter is a {@link module:engine/model/item~Item model item}.
-	 */
-	insert( item, itemOrPosition, offset ) {
-		const position = Position.createAt( itemOrPosition, offset );
-
-		// For text that has no parent we need to make a WeakInsert.
-		const delta = item instanceof Text && !item.parent ? new WeakInsertDelta() : new InsertDelta();
-
-		// If item has a parent already.
-		if ( item.parent ) {
-			// We need to check if item is going to be inserted within the same document.
-			if ( isSameTree( item.root, position.root ) ) {
-				// If it's we just need to move it.
-				this.move( Range.createOn( item ), position );
-
-				return;
-			}
-			// If it isn't the same root.
-			else {
-				// We need to remove this item from old position first.
-				this.remove( item );
-			}
-		}
-
-		const insert = new InsertOperation( position, item, this.document.version );
-
-		this.addDelta( delta );
-		delta.addOperation( insert );
-		this.document.applyOperation( insert );
-
-		// When element is a DocumentFragment we need to move its markers to Document#markers.
-		if ( item instanceof DocumentFragment ) {
-			for ( const [ markerName, markerRange ] of item.markers ) {
-				// We need to migrate marker range from DocumentFragment to Document.
-				const rangeRootPosition = Position.createAt( markerRange.root );
-				const range = new Range(
-					markerRange.start._getCombined( rangeRootPosition, position ),
-					markerRange.end._getCombined( rangeRootPosition, position )
-				);
-
-				this.setMarker( markerName, range );
-			}
-		}
-	}
-
-	/**
-	 * Creates and inserts text on given position. You can optionally set text attributes:
-	 *
-	 *		batch.insertText( 'foo', position );
-	 *		batch.insertText( 'foo', { 'bold': true }, position );
-	 *
-	 * Instead of using position you can use parent and offset or define that text should be inserted at the end
-	 * or before or after other node:
-	 *
-	 * 		batch.insertText( 'foo', paragraph, 5 ); // inserts in paragraph, at offset 5
-	 *		batch.insertText( 'foo', paragraph, 'end' ); // inserts at the end of the paragraph
-	 *		batch.insertText( 'foo', image, 'after' ); // inserts after image
-	 *
-	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
-	 *
-	 * @param {String} data Text data.
-	 * @param {Object} [attributes] Text attributes.
-	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
-	 * third parameter is a {@link module:engine/model/item~Item model item}.
-	 */
-	insertText( text, attributes, itemOrPosition, offset ) {
-		if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
-			this.insert( this.createText( text ), attributes, itemOrPosition );
-		} else {
-			this.insert( this.createText( text, attributes ), itemOrPosition, offset );
-		}
-	}
-
-	/**
-	 * Creates and inserts element on given position. You can optionally set attributes:
-	 *
-	 *		batch.insertElement( 'paragraph', position );
-	 *		batch.insertElement( 'paragraph', { 'alignment': 'center' }, position );
-	 *
-	 * Instead of using position you can use parent and offset or define that text should be inserted at the end
-	 * or before or after other node:
-	 *
-	 * 		batch.insertElement( 'paragraph', paragraph, 5 ); // inserts in paragraph, at offset 5
-	 *		batch.insertElement( 'paragraph', blockquote, 'end' ); // insets at the end of the blockquote
-	 *		batch.insertElement( 'paragraph', image, 'after' ); // inserts after image
-	 *
-	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
-	 *
-	 * @param {String} name Name of the element.
-	 * @param {Object} [attributes] Elements attributes.
-	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
-	 * third parameter is a {@link module:engine/model/item~Item model item}.
-	 */
-	insertElement( name, attributes, itemOrPosition, offset ) {
-		if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
-			this.insert( this.createElement( name ), attributes, itemOrPosition );
-		} else {
-			this.insert( this.createElement( name, attributes ), itemOrPosition, offset );
-		}
-	}
-
-	/**
-	 * Inserts item at the end of the given parent.
-	 *
-	 *		const paragraph = batch.createElement( 'paragraph' );
-	 *		batch.append( paragraph, root );
-	 *
-	 * Note that if the item already has parent it will be removed from the previous parent.
-	 *
-	 * If you want to move {@link module:engine/model/range~Range range} instead of an
-	 * {@link module:engine/model/item~Item item} use {@link module:engine/model/batch~Batch#move move}.
-	 *
-	 * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
-	 * item Item or document fragment to insert.
-	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
-	 */
-	append( item, parent ) {
-		this.insert( item, parent, 'end' );
-	}
-
-	/**
-	 * Creates text node and inserts it at the end of the parent. You can optionally set text attributes:
-	 *
-	 *		batch.appendText( 'foo', paragraph );
-	 *		batch.appendText( 'foo', { 'bold': true }, paragraph );
-	 *
-	 * @param {String} text Text data.
-	 * @param {Object} [attributes] Text attributes.
-	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
-	 */
-	appendText( text, attributes, parent ) {
-		if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
-			this.insert( this.createText( text ), attributes, 'end' );
-		} else {
-			this.insert( this.createText( text, attributes ), parent, 'end' );
-		}
-	}
-
-	/**
-	 * Creates element and inserts it at the end of the parent. You can optionally set attributes:
-	 *
-	 *		batch.appendElement( 'paragraph', root );
-	 *		batch.appendElement( 'paragraph', { 'alignment': 'center' }, root );
-	 *
-	 * @param {String} name Name of the element.
-	 * @param {Object} [attributes] Elements attributes.
-	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
-	 */
-	appendElement( name, attributes, parent ) {
-		if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
-			this.insert( this.createElement( name ), attributes, 'end' );
-		} else {
-			this.insert( this.createElement( name, attributes ), parent, 'end' );
-		}
-	}
-
-	/**
-	 * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
-	 * or on a {@link module:engine/model/range~Range range}.
-	 *
-	 * @param {String} key Attribute key.
-	 * @param {*} value Attribute new value.
-	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
-	 * Model item or range on which the attribute will be set.
-	 */
-	setAttribute( key, value, itemOrRange ) {
-		if ( itemOrRange instanceof Range ) {
-			setAttributeToRange( this, key, value, itemOrRange );
-		} else {
-			setAttributeToItem( this, key, value, itemOrRange );
-		}
-	}
-
-	/**
-	 * Sets values of attributes on a {@link module:engine/model/item~Item model item}
-	 * or on a {@link module:engine/model/range~Range range}.
-	 *
-	 *		batch.setAttributes( {
-	 *			'bold': true,
-	 *			'italic': true
-	 *		}, range );
-	 *
-	 * @param {Object} attributes Attributes keys and values.
-	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
-	 * Model item or range on which the attributes will be set.
-	 */
-	setAttributes( attributes, itemOrRange ) {
-		for ( const [ key, val ] of toMap( attributes ) ) {
-			this.setAttribute( key, val, itemOrRange );
-		}
-	}
-
-	/**
-	 * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
-	 * or from a {@link module:engine/model/range~Range range}.
-	 *
-	 * @param {String} key Attribute key.
-	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
-	 * Model item or range from which the attribute will be removed.
-	 */
-	removeAttribute( key, itemOrRange ) {
-		if ( itemOrRange instanceof Range ) {
-			setAttributeToRange( this, key, null, itemOrRange );
-		} else {
-			setAttributeToItem( this, key, null, itemOrRange );
-		}
-	}
-
-	/**
-	 * Removes all attributes from all elements in the range or from the given item.
-	 *
-	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
-	 * Model item or range from which all attributes will be removed.
-	 */
-	clearAttributes( itemOrRange ) {
-		const removeAttributesFromItem = item => {
-			for ( const attribute of item.getAttributeKeys() ) {
-				this.removeAttribute( attribute, item );
-			}
-		};
-
-		if ( !( itemOrRange instanceof Range ) ) {
-			removeAttributesFromItem( itemOrRange );
-		} else {
-			for ( const item of itemOrRange.getItems() ) {
-				removeAttributesFromItem( item );
-			}
-		}
-	}
-
-	/**
-	 * Moves all items in the source range to the target position.
-	 *
-	 *		batch.move( sourceRange, targetPosition );
-	 *
-	 * Instead of the target position you can use parent and offset or define that range should be moved to the end
-	 * or before or after chosen item:
-	 *
-	 * 		batch.move( sourceRange, paragraph, 5 ); // moves all items in the range to the paragraph at offset 5
-	 *		batch.move( sourceRange, blockquote, 'end' ); // moves all items in the range at the end of the blockquote
-	 *		batch.move( sourceRange, image, 'after' ); // moves all items in the range after the image
-	 *
-	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
-	 *
-	 * Note that items can be moved only within the same tree. It means that you can move items within the same root
-	 * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
-	 * but you can not move items from document fragment to the document or from one detached element to another. Use
-	 * {@link module:engine/model/batch~Batch#insert} in such cases.
-	 *
-	 * @param {module:engine/model/range~Range} range Source range.
-	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
-	 * second parameter is a {@link module:engine/model/item~Item model item}.
-	 */
-	move( range, itemOrPosition, offset ) {
-		if ( !( range instanceof Range ) ) {
-			/**
-			 * Invalid range to move.
-			 *
-			 * @error batch-move-invalid-range
-			 */
-			throw new CKEditorError( 'batch-move-invalid-range: Invalid range to move.' );
-		}
-
-		if ( !range.isFlat ) {
-			/**
-			 * Range to move is not flat.
-			 *
-			 * @error batch-move-range-not-flat
-			 */
-			throw new CKEditorError( 'batch-move-range-not-flat: Range to move is not flat.' );
-		}
-
-		const position = Position.createAt( itemOrPosition, offset );
-
-		if ( !isSameTree( range.root, position.root ) ) {
-			/**
-			 * Range is going to be moved within not the same document. Please use
-			 * {@link module:engine/model/batch~Batch#insert insert} instead.
-			 *
-			 * @error batch-move-different-document
-			 */
-			throw new CKEditorError( 'batch-move-different-document: Range is going to be moved between different documents.' );
-		}
-
-		const delta = new MoveDelta();
-		this.addDelta( delta );
-
-		const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, this.document.version );
-		delta.addOperation( operation );
-		this.document.applyOperation( operation );
-	}
-
-	/**
-	 * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}.
-	 *
-	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
-	 */
-	remove( itemOrRange ) {
-		const addRemoveDelta = ( position, howMany ) => {
-			const delta = new RemoveDelta();
-			this.addDelta( delta );
-			let operation;
-
-			if ( position.root.document ) {
-				const graveyard = this.document.graveyard;
-				const gyPosition = new Position( graveyard, [ 0 ] );
-
-				operation = new RemoveOperation( position, howMany, gyPosition, this.document.version );
-			} else {
-				operation = new DetachOperation( position, howMany, this.document.version );
-			}
-
-			delta.addOperation( operation );
-			this.document.applyOperation( operation );
-		};
-
-		if ( itemOrRange instanceof Range ) {
-			// The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
-			const ranges = itemOrRange.getMinimalFlatRanges().reverse();
-
-			for ( const flat of ranges ) {
-				addRemoveDelta( flat.start, flat.end.offset - flat.start.offset );
-			}
-		} else {
-			const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
-
-			addRemoveDelta( Position.createBefore( itemOrRange ), howMany );
-		}
-	}
-
-	/**
-	 * Merges two siblings at the given position.
-	 *
-	 * Node before and after the position have to be an element. Otherwise `batch-merge-no-element-before` or
-	 * `batch-merge-no-element-after` error will be thrown.
-	 *
-	 * @param {module:engine/model/position~Position} position Position of merge.
-	 */
-	merge( position ) {
-		const delta = new MergeDelta();
-		this.addDelta( delta );
-
-		const nodeBefore = position.nodeBefore;
-		const nodeAfter = position.nodeAfter;
-
-		if ( !( nodeBefore instanceof Element ) ) {
-			/**
-			 * Node before merge position must be an element.
-			 *
-			 * @error batch-merge-no-element-before
-			 */
-			throw new CKEditorError( 'batch-merge-no-element-before: Node before merge position must be an element.' );
-		}
-
-		if ( !( nodeAfter instanceof Element ) ) {
-			/**
-			 * Node after merge position must be an element.
-			 *
-			 * @error batch-merge-no-element-after
-			 */
-			throw new CKEditorError( 'batch-merge-no-element-after: Node after merge position must be an element.' );
-		}
-
-		const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
-		const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset );
-
-		const move = new MoveOperation(
-			positionAfter,
-			nodeAfter.maxOffset,
-			positionBefore,
-			this.document.version
-		);
-
-		move.isSticky = true;
-		delta.addOperation( move );
-		this.document.applyOperation( move );
-
-		const graveyard = this.document.graveyard;
-		const gyPosition = new Position( graveyard, [ 0 ] );
-
-		const remove = new RemoveOperation( position, 1, gyPosition, this.document.version );
-		delta.addOperation( remove );
-		this.document.applyOperation( remove );
-	}
-
-	/**
-	 * Renames given element.
-	 *
-	 * @param {module:engine/model/element~Element} element The element to rename.
-	 * @param {String} newName New element name.
-	 */
-	rename( element, newName ) {
-		if ( !( element instanceof Element ) ) {
-			/**
-			 * Trying to rename an object which is not an instance of Element.
-			 *
-			 * @error batch-rename-not-element-instance
-			 */
-			throw new CKEditorError( 'batch-rename-not-element-instance: Trying to rename an object which is not an instance of Element.' );
-		}
-
-		const delta = new RenameDelta();
-		this.addDelta( delta );
-
-		const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, this.document.version );
-		delta.addOperation( renameOperation );
-		this.document.applyOperation( renameOperation );
-	}
-
-	/**
-	 * Splits an element at the given position.
-	 *
-	 * The element needs to have a parent. It cannot be a root element nor document fragment.
-	 * The `batch-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.
-	 */
-	split( position ) {
-		const delta = new SplitDelta();
-		this.addDelta( delta );
-
-		const splitElement = position.parent;
-
-		if ( !splitElement.parent ) {
-			/**
-			 * Element with no parent can not be split.
-			 *
-			 * @error batch-split-element-no-parent
-			 */
-			throw new CKEditorError( 'batch-split-element-no-parent: Element with no parent can not be split.' );
-		}
-
-		const copy = new Element( splitElement.name, splitElement.getAttributes() );
-
-		const insert = new InsertOperation(
-			Position.createAfter( splitElement ),
-			copy,
-			this.document.version
-		);
-
-		delta.addOperation( insert );
-		this.document.applyOperation( insert );
-
-		const move = new MoveOperation(
-			position,
-			splitElement.maxOffset - position.offset,
-			Position.createFromParentAndOffset( copy, 0 ),
-			this.document.version
-		);
-		move.isSticky = true;
-
-		delta.addOperation( move );
-		this.document.applyOperation( move );
-	}
-
-	/**
-	 * Wraps given range with given element or with a new element with specified name, if string has been passed.
-	 *
-	 * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat}). If not, error will be thrown.
-	 *
-	 * @param {module:engine/model/range~Range} range Range to wrap.
-	 * @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with.
-	 */
-	wrap( range, elementOrString ) {
-		if ( !range.isFlat ) {
-			/**
-			 * Range to wrap is not flat.
-			 *
-			 * @error batch-wrap-range-not-flat
-			 */
-			throw new CKEditorError( 'batch-wrap-range-not-flat: Range to wrap is not flat.' );
-		}
-
-		const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
-
-		if ( element.childCount > 0 ) {
-			/**
-			 * Element to wrap with is not empty.
-			 *
-			 * @error batch-wrap-element-not-empty
-			 */
-			throw new CKEditorError( 'batch-wrap-element-not-empty: Element to wrap with is not empty.' );
-		}
-
-		if ( element.parent !== null ) {
-			/**
-			 * Element to wrap with is already attached to a tree model.
-			 *
-			 * @error batch-wrap-element-attached
-			 */
-			throw new CKEditorError( 'batch-wrap-element-attached: Element to wrap with is already attached to tree model.' );
-		}
-
-		const delta = new WrapDelta();
-		this.addDelta( delta );
-
-		const insert = new InsertOperation( range.end, element, this.document.version );
-		delta.addOperation( insert );
-		this.document.applyOperation( insert );
-
-		const targetPosition = Position.createFromParentAndOffset( element, 0 );
-		const move = new MoveOperation(
-			range.start,
-			range.end.offset - range.start.offset,
-			targetPosition,
-			this.document.version
-		);
-		delta.addOperation( move );
-		this.document.applyOperation( move );
-	}
-
-	/**
-	 * Unwraps children of the given element – all its children are moved before it and then the element is removed.
-	 * Throws error if you try to unwrap an element which does not have a parent.
-	 *
-	 * @param {module:engine/model/element~Element} element Element to unwrap.
-	 */
-	unwrap( element ) {
-		if ( element.parent === null ) {
-			/**
-			 * Trying to unwrap an element which has no parent.
-			 *
-			 * @error batch-unwrap-element-no-parent
-			 */
-			throw new CKEditorError( 'batch-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
-		}
-
-		const delta = new UnwrapDelta();
-		this.addDelta( delta );
-
-		const sourcePosition = Position.createFromParentAndOffset( element, 0 );
-
-		const move = new MoveOperation(
-			sourcePosition,
-			element.maxOffset,
-			Position.createBefore( element ),
-			this.document.version
-		);
-
-		move.isSticky = true;
-		delta.addOperation( move );
-		this.document.applyOperation( move );
-
-		// Computing new position because we moved some nodes before `element`.
-		// If we would cache `Position.createBefore( element )` we remove wrong node.
-		const graveyard = this.document.graveyard;
-		const gyPosition = new Position( graveyard, [ 0 ] );
-
-		const remove = new RemoveOperation( Position.createBefore( element ), 1, gyPosition, this.document.version );
-		delta.addOperation( remove );
-		this.document.applyOperation( remove );
-	}
-
-	/**
-	 * Adds or updates {@link module:engine/model/markercollection~Marker marker} with given name to given `range`.
-	 *
-	 * If passed name is a name of already existing marker (or {@link module:engine/model/markercollection~Marker Marker} instance
-	 * is passed), `range` parameter may be omitted. In this case marker will not be updated in
-	 * {@link module:engine/model/document~Document#markers document marker collection}. However the marker will be added to
-	 * the document history. This may be important for other features, like undo. From document history point of view, it will
-	 * look like the marker was created and added to the document at the moment when it is set using this method.
-	 *
-	 * This is useful if the marker is created before it can be added to document history (e.g. a feature creating the marker
-	 * is waiting for additional data, etc.). In this case, the marker may be first created directly through
-	 * {@link module:engine/model/markercollection~MarkerCollection MarkerCollection API} and only later added using `Batch` API.
-	 *
-	 * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to add or update.
-	 * @param {module:engine/model/range~Range} [newRange] Marker range.
-	 */
-	setMarker( markerOrName, newRange ) {
-		const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
-		const currentMarker = this.document.markers.get( name );
-
-		if ( !newRange && !currentMarker ) {
-			/**
-			 * Range parameter is required when adding a new marker.
-			 *
-			 * @error batch-setMarker-no-range
-			 */
-			throw new CKEditorError( 'batch-setMarker-no-range: Range parameter is required when adding a new marker.' );
-		}
-
-		const currentRange = currentMarker ? currentMarker.getRange() : null;
-
-		if ( !newRange ) {
-			// If `newRange` is not given, treat this as synchronizing existing marker.
-			// Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
-			addMarkerOperation( this, name, null, currentRange );
-		} else {
-			// Just change marker range.
-			addMarkerOperation( this, name, currentRange, newRange );
-		}
-	}
-
-	/**
-	 * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
-	 *
-	 * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
-	 */
-	removeMarker( markerOrName ) {
-		const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
-
-		if ( !this.document.markers.has( name ) ) {
-			/**
-			 * Trying to remove marker which does not exist.
-			 *
-			 * @error batch-removeMarker-no-marker
-			 */
-			throw new CKEditorError( 'batch-removeMarker-no-marker: Trying to remove marker which does not exist.' );
-		}
-
-		const oldRange = this.document.markers.get( name ).getRange();
-
-		addMarkerOperation( this, name, oldRange, null );
-	}
-}
-
-// Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
-//
-// Because attribute operation needs to have the same attribute value on the whole range, this function splits
-// the range into smaller parts.
-//
-// @private
-// @param {module:engine/model/batch~Batch} batch
-// @param {String} key Attribute key.
-// @param {*} value Attribute new value.
-// @param {module:engine/model/range~Range} range Model range on which the attribute will be set.
-function setAttributeToRange( batch, key, value, range ) {
-	const delta = new AttributeDelta();
-	const doc = batch.document;
-
-	// Position of the last split, the beginning of the new range.
-	let lastSplitPosition = range.start;
-
-	// Currently position in the scanning range. Because we need value after the position, it is not a current
-	// position of the iterator but the previous one (we need to iterate one more time to get the value after).
-	let position;
-
-	// Value before the currently position.
-	let valueBefore;
-
-	// Value after the currently position.
-	let valueAfter;
-
-	for ( const val of range ) {
-		valueAfter = val.item.getAttribute( key );
-
-		// At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
-		// because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
-		if ( position && valueBefore != valueAfter ) {
-			// if valueBefore == value there is nothing to change, so we add operation only if these values are different.
-			if ( valueBefore != value ) {
-				addOperation();
-			}
-
-			lastSplitPosition = position;
-		}
-
-		position = val.nextPosition;
-		valueBefore = valueAfter;
-	}
-
-	// Because position in the loop is not the iterator position (see let position comment), the last position in
-	// the while loop will be last but one position in the range. We need to check the last position manually.
-	if ( position instanceof Position && position != lastSplitPosition && valueBefore != value ) {
-		addOperation();
-	}
-
-	function addOperation() {
-		// Add delta to the batch only if there is at least operation in the delta. Add delta only once.
-		if ( delta.operations.length === 0 ) {
-			batch.addDelta( delta );
-		}
-
-		const range = new Range( lastSplitPosition, position );
-		const operation = new AttributeOperation( range, key, valueBefore, value, doc.version );
-
-		delta.addOperation( operation );
-		doc.applyOperation( operation );
-	}
-}
-
-// Sets given attribute to the given node. When attribute value is null then attribute will be removed.
-//
-// @private
-// @param {module:engine/model/batch~Batch} batch
-// @param {String} key Attribute key.
-// @param {*} value Attribute new value.
-// @param {module:engine/model/item~Item} item Model item on which the attribute will be set.
-function setAttributeToItem( batch, key, value, item ) {
-	const doc = batch.document;
-	const previousValue = item.getAttribute( key );
-	let range, operation;
-
-	if ( previousValue != value ) {
-		const delta = item.root === item ? new RootAttributeDelta() : new AttributeDelta();
-		batch.addDelta( delta );
-
-		if ( item.root === item ) {
-			// If we change attributes of root element, we have to use `RootAttributeOperation`.
-			operation = new RootAttributeOperation( item, key, previousValue, value, doc.version );
-		} else {
-			if ( item.is( 'element' ) ) {
-				// If we change the attribute of the element, we do not want to change attributes of its children, so
-				// the end of the range cannot be after the closing tag, it should be inside that element, before any of
-				// it's children, so the range will contain only the opening tag.
-				range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
-			} else {
-				// If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
-				// all characters represented by it.
-				range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
-			}
-
-			operation = new AttributeOperation( range, key, previousValue, value, doc.version );
-		}
-
-		delta.addOperation( operation );
-		doc.applyOperation( operation );
-	}
-}
-
-// Creates and adds marker operation to {@link module:engine/model/delta/delta~Delta delta}.
-//
-// @private
-// @param {module:engine/model/batch~Batch} batch
-// @param {String} name Marker name.
-// @param {module:engine/model/range~Range} oldRange Marker range before the change.
-// @param {module:engine/model/range~Range} newRange Marker range after the change.
-function addMarkerOperation( batch, name, oldRange, newRange ) {
-	const doc = batch.document;
-	const delta = new MarkerDelta();
-
-	const operation = new MarkerOperation( name, oldRange, newRange, doc.markers, doc.version );
-
-	batch.addDelta( delta );
-	delta.addOperation( operation );
-	doc.applyOperation( operation );
-}
-
-// Returns `true` if both root elements are the same element or both are documents root elements.
-//
-// Elements in the same tree can be moved (for instance you can move element form one documents root to another, or
-// within the same document fragment), but when element supposed to be moved from document fragment to the document, or
-// to another document it should be removed and inserted to avoid problems with OT. This is because features like undo or
-// collaboration may track changes on the document but ignore changes on detached fragments and should not get
-// unexpected `move` operation.
-function isSameTree( rootA, rootB ) {
-	// If it is the same root this is the same tree.
-	if ( rootA === rootB ) {
-		return true;
-	}
-
-	// If both roots are documents root it is operation within the document what we still treat as the same tree.
-	if ( rootA instanceof RootElement && rootB instanceof RootElement ) {
-		return true;
-	}
-
-	return false;
 }

+ 1 - 1
packages/ckeditor5-engine/src/model/delta/attributedelta.js

@@ -14,7 +14,7 @@ import Range from '../range';
 
 /**
  * To provide specific OT behavior and better collisions solving, methods to change attributes
- * ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute})
+ * ({@link module:engine/model/writer~Writer#setAttribute} and {@link module:engine/model/writer~Writer#removeAttribute})
  * use `AttributeDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta

+ 1 - 1
packages/ckeditor5-engine/src/model/delta/insertdelta.js

@@ -12,7 +12,7 @@ import RemoveDelta from './removedelta';
 import DeltaFactory from './deltafactory';
 
 /**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert Batch#insert} method
+ * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#insert Batch#insert} method
  * uses the `InsertDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta

+ 2 - 2
packages/ckeditor5-engine/src/model/delta/markerdelta.js

@@ -11,8 +11,8 @@ import Delta from './delta';
 import DeltaFactory from './deltafactory';
 
 /**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#setMarker Batch#setMarker}
- * and {@link module:engine/model/batch~Batch#removeMarker Batch#removeMarker} methods use the `MarkerDelta` class which inherits
+ * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#setMarker Batch#setMarker}
+ * and {@link module:engine/model/writer~Writer#removeMarker Batch#removeMarker} methods use the `MarkerDelta` class which inherits
  * from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta

+ 1 - 1
packages/ckeditor5-engine/src/model/delta/mergedelta.js

@@ -12,7 +12,7 @@ import DeltaFactory from './deltafactory';
 import SplitDelta from './splitdelta';
 
 /**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
+ * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#merge} method
  * uses the `MergeDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta

+ 1 - 1
packages/ckeditor5-engine/src/model/delta/movedelta.js

@@ -11,7 +11,7 @@ import Delta from './delta';
 import DeltaFactory from './deltafactory';
 
 /**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#move} method
+ * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#move} method
  * uses the `MoveDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta

+ 1 - 1
packages/ckeditor5-engine/src/model/delta/removedelta.js

@@ -11,7 +11,7 @@ import MoveDelta from './movedelta';
 import DeltaFactory from './deltafactory';
 
 /**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#remove} method
+ * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#remove} method
  * uses the `RemoveDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta

+ 1 - 1
packages/ckeditor5-engine/src/model/delta/renamedelta.js

@@ -11,7 +11,7 @@ import Delta from './delta';
 import DeltaFactory from './deltafactory';
 
 /**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#rename Batch#rename} method
+ * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#rename Batch#rename} method
  * uses the `RenameDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta

+ 1 - 1
packages/ckeditor5-engine/src/model/delta/rootattributedelta.js

@@ -12,7 +12,7 @@ import DeltaFactory from './deltafactory';
 
 /**
  * To provide specific OT behavior and better collisions solving, methods to change attributes
- * ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute})
+ * ({@link module:engine/model/writer~Writer#setAttribute} and {@link module:engine/model/writer~Writer#removeAttribute})
  * use `RootAttributeDelta` class which inherits from the `Delta` class and may
  * overwrite some methods.
  *

+ 1 - 1
packages/ckeditor5-engine/src/model/delta/splitdelta.js

@@ -13,7 +13,7 @@ import MoveOperation from '../operation/moveoperation';
 import MergeDelta from '../delta/mergedelta';
 
 /**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#split} method
+ * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#split} method
  * uses `SplitDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta

+ 1 - 1
packages/ckeditor5-engine/src/model/delta/unwrapdelta.js

@@ -12,7 +12,7 @@ import DeltaFactory from './deltafactory';
 import WrapDelta from './wrapdelta';
 
 /**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
+ * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#merge} method
  * uses the `UnwrapDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta

+ 1 - 1
packages/ckeditor5-engine/src/model/delta/weakinsertdelta.js

@@ -11,7 +11,7 @@ import InsertDelta from './insertdelta';
 import DeltaFactory from './deltafactory';
 
 /**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert} method
+ * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#insert} method
  * uses the `WeakInsertDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta

+ 1 - 1
packages/ckeditor5-engine/src/model/delta/wrapdelta.js

@@ -13,7 +13,7 @@ import UnwrapDelta from './unwrapdelta';
 import Range from '../range';
 
 /**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
+ * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#merge} method
  * uses the `WrapDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta

+ 45 - 103
packages/ckeditor5-engine/src/model/document.js

@@ -14,12 +14,9 @@ import './delta/basic-transformations';
 import Range from './range';
 import Position from './position';
 import RootElement from './rootelement';
-import Batch from './batch';
 import History from './history';
 import DocumentSelection from './documentselection';
-import Schema from './schema';
 import TreeWalker from './treewalker';
-import MarkerCollection from './markercollection';
 import deltaTransform from './delta/transform';
 import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
@@ -34,14 +31,6 @@ const graveyardName = '$graveyard';
  * {@link module:engine/model/document~Document#roots root elements}, for example if the editor have multiple editable areas,
  * each area will be represented by the separate root.
  *
- * All changes in the document are done by {@link module:engine/model/operation/operation~Operation operations}. To create operations in
- * a simple way, use the {@link module:engine/model/batch~Batch} API, for example:
- *
- *		const batch = doc.batch();
- *		batch.insert( node, position );
- *		batch.split( otherPosition );
- *
- * @see module:engine/model/document~Document#batch
  * @mixes module:utils/emittermixin~EmitterMixin
  */
 export default class Document {
@@ -49,7 +38,15 @@ export default class Document {
 	 * Creates an empty document instance with no {@link #roots} (other than
 	 * the {@link #graveyard graveyard root}).
 	 */
-	constructor() {
+	constructor( model ) {
+		/**
+		 * {@link module:engine/model/model~Model} the document is part of.
+		 *
+		 * @readonly
+		 * @member {module:engine/model/model~Model}
+		 */
+		this.model = model;
+
 		/**
 		 * Document version. It starts from `0` and every operation increases the version number. It is used to ensure that
 		 * operations are applied on the proper document version.
@@ -61,13 +58,6 @@ export default class Document {
 		 */
 		this.version = 0;
 
-		/**
-		 * Schema for this document.
-		 *
-		 * @member {module:engine/model/schema~Schema}
-		 */
-		this.schema = new Schema();
-
 		/**
 		 * Document's history.
 		 *
@@ -78,14 +68,6 @@ export default class Document {
 		 */
 		this.history = new History( this );
 
-		/**
-		 * Document's markers' collection.
-		 *
-		 * @readonly
-		 * @member {module:engine/model/markercollection~MarkerCollection}
-		 */
-		this.markers = new MarkerCollection();
-
 		/**
 		 * Selection done on this document.
 		 *
@@ -94,14 +76,6 @@ export default class Document {
 		 */
 		this.selection = new DocumentSelection( this );
 
-		/**
-		 * Array of pending changes. See: {@link #enqueueChanges}.
-		 *
-		 * @private
-		 * @member {Array.<Function>}
-		 */
-		this._pendingChanges = [];
-
 		/**
 		 * List of roots that are owned and managed by this document. Use {@link #createRoot} and
 		 * {@link #getRoot} to manipulate it.
@@ -130,6 +104,35 @@ export default class Document {
 
 		// Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
 		this.createRoot( '$root', graveyardName );
+
+		this.listenTo( model, 'applyOperation', ( evt, args ) => {
+			const operation = args[ 0 ];
+
+			if ( operation.isDocumentOperation && operation.baseVersion !== this.version ) {
+				/**
+				 * Only operations with matching versions can be applied.
+				 *
+				 * @error document-applyOperation-wrong-version
+				 * @param {module:engine/model/operation/operation~Operation} operation
+				 */
+				throw new CKEditorError(
+					'model-document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
+					{ operation } );
+			}
+		}, { priority: 'high' } );
+
+		this.listenTo( model, 'applyOperation', ( evt, args ) => {
+			const operation = args[ 0 ];
+
+			if ( operation.isDocumentOperation ) {
+				this.version++;
+				this.history.addDelta( operation.delta );
+				this.fire( 'change', operation.type, evt.return, operation.delta.batch, operation.delta.type );
+			}
+		}, { priority: 'low' } );
+
+		// Temporary compatibility.
+		model.delegate( 'changesDone' ).to( this );
 	}
 
 	/**
@@ -142,46 +145,6 @@ export default class Document {
 		return this.getRoot( graveyardName );
 	}
 
-	/**
-	 * This is the entry point for all document changes. All changes on the document are done using
-	 * {@link module:engine/model/operation/operation~Operation operations}. To create operations in the simple way use the
-	 * {@link module:engine/model/batch~Batch} API available via {@link #batch} method.
-	 *
-	 * @fires event:change
-	 * @param {module:engine/model/operation/operation~Operation} operation Operation to be applied.
-	 */
-	applyOperation( operation ) {
-		if ( operation.baseVersion !== this.version ) {
-			/**
-			 * Only operations with matching versions can be applied.
-			 *
-			 * @error document-applyOperation-wrong-version
-			 * @param {module:engine/model/operation/operation~Operation} operation
-			 */
-			throw new CKEditorError(
-				'model-document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
-				{ operation } );
-		}
-
-		const changes = operation._execute();
-
-		if ( operation.isDocumentOperation ) {
-			this.version++;
-			this.history.addDelta( operation.delta );
-			this.fire( 'change', operation.type, changes, operation.delta.batch, operation.delta.type );
-		}
-	}
-
-	/**
-	 * Creates a {@link module:engine/model/batch~Batch} instance which allows to change the document.
-	 *
-	 * @param {String} [type] Batch type. See {@link module:engine/model/batch~Batch#type}.
-	 * @returns {module:engine/model/batch~Batch} Batch instance.
-	 */
-	batch( type ) {
-		return new Batch( this, type );
-	}
-
 	/**
 	 * Creates a new top-level root.
 	 *
@@ -220,30 +183,6 @@ export default class Document {
 		this.stopListening();
 	}
 
-	/**
-	 * Enqueues document changes. Any changes to be done on document (mostly using {@link #batch}
-	 * should be placed in the queued callback. If no other plugin is changing document at the moment, the callback will be
-	 * called immediately. Otherwise it will wait for all previously queued changes to finish happening. This way
-	 * queued callback will not interrupt other callbacks.
-	 *
-	 * When all queued changes are done {@link #event:changesDone} event is fired.
-	 *
-	 * @fires changesDone
-	 * @param {Function} callback Callback to enqueue.
-	 */
-	enqueueChanges( callback ) {
-		this._pendingChanges.push( callback );
-
-		if ( this._pendingChanges.length == 1 ) {
-			while ( this._pendingChanges.length ) {
-				this._pendingChanges[ 0 ]();
-				this._pendingChanges.shift();
-			}
-
-			this.fire( 'changesDone' );
-		}
-	}
-
 	/**
 	 * Returns top-level root by its name.
 	 *
@@ -306,8 +245,10 @@ export default class Document {
 	 * @returns {module:engine/model/range~Range|null} Nearest selection range or `null` if one cannot be found.
 	 */
 	getNearestSelectionRange( position, direction = 'both' ) {
+		const schema = this.model.schema;
+
 		// Return collapsed range if provided position is valid.
-		if ( this.schema.check( { name: '$text', inside: position } ) ) {
+		if ( schema.check( { name: '$text', inside: position } ) ) {
 			return new Range( position );
 		}
 
@@ -325,11 +266,11 @@ export default class Document {
 			const type = ( data.walker == backwardWalker ? 'elementEnd' : 'elementStart' );
 			const value = data.value;
 
-			if ( value.type == type && this.schema.objects.has( value.item.name ) ) {
+			if ( value.type == type && schema.objects.has( value.item.name ) ) {
 				return Range.createOn( value.item );
 			}
 
-			if ( this.schema.check( { name: '$text', inside: value.nextPosition } ) ) {
+			if ( schema.check( { name: '$text', inside: value.nextPosition } ) ) {
 				return new Range( value.nextPosition );
 			}
 		}
@@ -367,6 +308,7 @@ export default class Document {
 
 		// Due to circular references we need to remove parent reference.
 		json.selection = '[engine.model.DocumentSelection]';
+		json.model = '[engine.model.Model]';
 
 		return json;
 	}
@@ -458,7 +400,7 @@ export default class Document {
 	 */
 
 	/**
-	 * Fired when all queued document changes are done. See {@link #enqueueChanges}.
+	 * Fired when all queued document changes are done. See {@link module:engine/model/model~Model#change}.
 	 *
 	 * @event changesDone
 	 */

+ 4 - 4
packages/ckeditor5-engine/src/model/documentfragment.js

@@ -17,8 +17,8 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  * can be seen as siblings. In other words, it is a detached part of model tree, without a root.
  *
  * DocumentFragment has own {@link module:engine/model/markercollection~MarkerCollection}. Markers from this collection
- * will be set to the {@link module:engine/model/document~Document#markers document markers} by a
- * {@linkTODO module:engine/model/writer~writer.insert} function.
+ * will be set to the {@link module:engine/model/model~Model#markers model markers} by a
+ * {@link module:engine/model/writer~Writer#insert} function.
  */
 export default class DocumentFragment {
 	/**
@@ -30,10 +30,10 @@ export default class DocumentFragment {
 	constructor( children ) {
 		/**
 		 * DocumentFragment static markers map. This is a list of names and {@link module:engine/model/range~Range ranges}
-		 * which will be set as Markers to {@link module:engine/model/document~Document#markers document markers collection}
+		 * which will be set as Markers to {@link module:engine/model/model~Model#markers model markers collection}
 		 * when DocumentFragment will be inserted to the document.
 		 *
-		 * @member {Map<String, {module:engine/model/range~Range}>} module:engine/model/documentfragment~DocumentFragment#markers
+		 * @member {Map<String,module:engine/model/range~Range>} module:engine/model/documentfragment~DocumentFragment#markers
 		 */
 		this.markers = new Map();
 

+ 38 - 27
packages/ckeditor5-engine/src/model/documentselection.js

@@ -60,7 +60,15 @@ export default class DocumentSelection extends Selection {
 		 * Document which owns this selection.
 		 *
 		 * @protected
-		 * @member {module:engine/model/document~Document} module:engine/model/documentselection~DocumentSelection#_document
+		 * @member {module:engine/model/model~Model}
+		 */
+		this._model = document.model;
+
+		/**
+		 * Document which owns this selection.
+		 *
+		 * @protected
+		 * @member {module:engine/model/document~Document}
 		 */
 		this._document = document;
 
@@ -83,9 +91,14 @@ export default class DocumentSelection extends Selection {
 				this._updateAttributes( false );
 			}
 
-			// Whenever element which had selection's attributes stored in it stops being empty,
-			// the attributes need to be removed.
-			clearAttributesStoredInElement( changes, batch, this._document );
+			// Batch may not be passed to the document#change event in some tests.
+			// See https://github.com/ckeditor/ckeditor5-engine/issues/1001#issuecomment-314202352
+			// Ignore also transparent batches because they are... transparent.
+			if ( batch && batch.type !== 'transparent' ) {
+				// Whenever element which had selection's attributes stored in it stops being empty,
+				// the attributes need to be removed.
+				clearAttributesStoredInElement( changes, this._model, batch );
+			}
 		} );
 	}
 
@@ -325,7 +338,7 @@ export default class DocumentSelection extends Selection {
 
 		const liveRange = LiveRange.createFromRange( range );
 
-		this.listenTo( liveRange, 'change:range', ( evt, oldRange, data ) => {
+		liveRange.on( 'change:range', ( evt, oldRange, data ) => {
 			// If `LiveRange` is in whole moved to the graveyard, fix that range.
 			if ( liveRange.root == this._document.graveyard ) {
 				this._fixGraveyardSelection( liveRange, data.sourcePosition );
@@ -543,7 +556,9 @@ export default class DocumentSelection extends Selection {
 	_removeStoredAttribute( key ) {
 		const storeKey = DocumentSelection._getStoreAttributeKey( key );
 
-		this._document.batch().removeAttribute( storeKey, this.anchor.parent );
+		this._model.change( writer => {
+			writer.removeAttribute( storeKey, this.anchor.parent );
+		} );
 	}
 
 	/**
@@ -556,7 +571,9 @@ export default class DocumentSelection extends Selection {
 	_storeAttribute( key, value ) {
 		const storeKey = DocumentSelection._getStoreAttributeKey( key );
 
-		this._document.batch().setAttribute( storeKey, value, this.anchor.parent );
+		this._model.change( writer => {
+			writer.setAttribute( storeKey, value, this.anchor.parent );
+		} );
 	}
 
 	/**
@@ -567,19 +584,20 @@ export default class DocumentSelection extends Selection {
 	 */
 	_setStoredAttributesTo( attrs ) {
 		const selectionParent = this.anchor.parent;
-		const batch = this._document.batch();
 
-		for ( const [ oldKey ] of this._getStoredAttributes() ) {
-			const storeKey = DocumentSelection._getStoreAttributeKey( oldKey );
+		this._model.change( writer => {
+			for ( const [ oldKey ] of this._getStoredAttributes() ) {
+				const storeKey = DocumentSelection._getStoreAttributeKey( oldKey );
 
-			batch.removeAttribute( storeKey, selectionParent );
-		}
+				writer.removeAttribute( storeKey, selectionParent );
+			}
 
-		for ( const [ key, value ] of attrs ) {
-			const storeKey = DocumentSelection._getStoreAttributeKey( key );
+			for ( const [ key, value ] of attrs ) {
+				const storeKey = DocumentSelection._getStoreAttributeKey( key );
 
-			batch.setAttribute( storeKey, value, selectionParent );
-		}
+				writer.setAttribute( storeKey, value, selectionParent );
+			}
+		} );
 	}
 
 	/**
@@ -592,7 +610,7 @@ export default class DocumentSelection extends Selection {
 	 */
 	_getSurroundingAttributes() {
 		const position = this.getFirstPosition();
-		const schema = this._document.schema;
+		const schema = this._model.schema;
 
 		let attrs = null;
 
@@ -711,14 +729,7 @@ function getAttrsIfCharacter( node ) {
 }
 
 // Removes selection attributes from element which is not empty anymore.
-function clearAttributesStoredInElement( changes, batch, document ) {
-	// Batch may not be passed to the document#change event in some tests.
-	// See https://github.com/ckeditor/ckeditor5-engine/issues/1001#issuecomment-314202352
-	// Ignore also transparent batches because they are... transparent.
-	if ( !batch || batch.type == 'transparent' ) {
-		return;
-	}
-
+function clearAttributesStoredInElement( changes, model, batch ) {
 	const changeParent = changes.range && changes.range.start.parent;
 
 	// `changes.range` is not set in case of rename, root and marker operations.
@@ -727,11 +738,11 @@ function clearAttributesStoredInElement( changes, batch, document ) {
 		return;
 	}
 
-	document.enqueueChanges( () => {
+	model.enqueueChange( batch, writer => {
 		const storedAttributes = Array.from( changeParent.getAttributeKeys() ).filter( key => key.startsWith( storePrefix ) );
 
 		for ( const key of storedAttributes ) {
-			batch.removeAttribute( key, changeParent );
+			writer.removeAttribute( key, changeParent );
 		}
 	} );
 }

+ 240 - 0
packages/ckeditor5-engine/src/model/model.js

@@ -0,0 +1,240 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/model
+ */
+
+import Batch from './batch';
+import Writer from './writer';
+import Schema from './schema';
+import Document from './document';
+import MarkerCollection from './markercollection';
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+
+/**
+ * Editors data model class. Model defines all data: either nodes users see in editable roots, grouped as the
+ * {@link module:engine/model/model~Model#document}, and all detached nodes, used to data manipulation. All of them are
+ * created and modified by the {@link module:engine/model/writer~Writer}, which can be get using
+ * {@link module:engine/model/model~Model#change} or {@link module:engine/model/model~Model#enqueueChange} methods.
+ */
+export default class Model {
+	constructor() {
+		/**
+		 * All callbacks added by {@link module:engine/model/model~Model#change} or
+		 * {@link module:engine/model/model~Model#enqueueChange} methods waiting to be executed.
+		 *
+		 * @private
+		 * @type {Array.<Function>}
+		 */
+		this._pendingChanges = [];
+
+		/**
+		 * Editors document model.
+		 *
+		 * @member {module:engine/model/document~Document}
+		 */
+		this.document = new Document( this );
+
+		/**
+		 * Schema for editors model.
+		 *
+		 * @member {module:engine/model/schema~Schema}
+		 */
+		this.schema = new Schema();
+
+		/**
+		 * Models markers' collection.
+		 *
+		 * @readonly
+		 * @member {module:engine/model/markercollection~MarkerCollection}
+		 */
+		this.markers = new MarkerCollection();
+
+		this.decorate( 'applyOperation' );
+	}
+
+	/**
+	 * Change method is the primary way of changing the model. You should use it to modify any node, including detached
+	 * nodes, not added to the {@link module:engine/model/model~Model#document}.
+	 *
+	 *		model.change( writer => {
+	 *			writer.insertText( 'foo', paragraph, 'end' );
+	 *		} );
+	 *
+	 * All changes inside the change block use the same {@link module:engine/model/batch~Batch} so share the same
+	 * undo step.
+	 *
+	 *		model.change( writer => {
+	 *			writer.insertText( 'foo', paragraph, 'end' ); // foo
+	 *
+	 *			model.change( writer => {
+	 *				writer.insertText( 'bar', paragraph, 'end' ); // foobar
+	 *			} );
+	 *
+	 * 			writer.insertText( 'bom', paragraph, 'end' ); // foobarbom
+	 *		} );
+	 *
+	 * Change block is executed imminently.
+	 *
+	 * You can also return a value from the change block.
+	 *
+	 *		const img = model.change( writer => {
+	 *			return writer.createElement( 'img' );
+	 *		} );
+	 *
+	 * When the outermost block is done the {@link #event:change} event is fired.
+	 *
+	 * @see #enqueueChange
+	 * @fires event:change
+	 * @fires event:changesDone
+	 * @param {Function} callback Callback function which may modify the model.
+	 * @returns {*} Value returned by the callback
+	 */
+	change( callback ) {
+		if ( this._pendingChanges.length === 0 ) {
+			this._pendingChanges.push( { batch: new Batch(), callback } );
+
+			return this._runPendingChanges()[ 0 ];
+		} else {
+			return callback( this._currentWriter );
+		}
+	}
+
+	/**
+	 * `enqueueChange` method is very similar to the {@link #change change method}, with two major differences.
+	 *
+	 * First, the callback of the `enqueueChange` is executed when all other changes are done. It might be executed
+	 * imminently if it is not nested in any other change block, but if it is nested in another change it will be delayed
+	 * and executed after the outermost block. If will be also executed after all previous `enqueueChange` blocks.
+	 *
+	 *		model.change( writer => {
+	 *			console.log( 1 );
+	 *
+	 *			model.enqueueChange( writer => {
+	 *				console.log( 3 );
+	 *			} );
+	 *
+	 * 			console.log( 2 );
+	 *		} );
+	 *
+	 * Second, it let you define the {@link module:engine/model/batch~Batch} to which you want to add your changes.
+	 * By default it creates a new batch, note that in the sample above `change` and `enqueueChange` blocks use a different
+	 * batch (and different {@link module:engine/model/writer~Writer} since each of them operates on the separate batch).
+	 *
+	 * Using `enqueueChange` block you can also add some changes to the batch you used before.
+	 *
+	 *		model.enqueueChange( batch, writer => {
+	 *			writer.insertText( 'foo', paragraph, 'end' );
+	 *		} );
+	 *
+	 * @fires event:change
+	 * @fires event:changesDone
+	 * @param {module:engine/model/batch~Batch|String} batchOrType Batch or batch type should be used in the callback.
+	 * If not defined new batch will be created.
+	 * @param {Function} callback Callback function which may modify the model.
+	 */
+	enqueueChange( batchOrType, callback ) {
+		if ( typeof batchOrType === 'string' ) {
+			batchOrType = new Batch( batchOrType );
+		} else if ( typeof batchOrType == 'function' ) {
+			callback = batchOrType;
+			batchOrType = new Batch();
+		}
+
+		this._pendingChanges.push( { batch: batchOrType, callback } );
+
+		if ( this._pendingChanges.length == 1 ) {
+			this._runPendingChanges();
+		}
+	}
+
+	/**
+	 * Common part of {@link module:engine/model/model~Model#change} and {@link module:engine/model/model~Model#enqueueChange}
+	 * which calls callbacks and returns array of values returned by these callbacks.
+	 *
+	 * @private
+	 * @returns {Array.<*>} Array of values returned by callbacks.
+	 */
+	_runPendingChanges() {
+		const ret = [];
+
+		while ( this._pendingChanges.length ) {
+			this._currentWriter = new Writer( this, this._pendingChanges[ 0 ].batch );
+
+			ret.push( this._pendingChanges[ 0 ].callback( this._currentWriter ) );
+
+			this.fire( 'change' );
+
+			this._pendingChanges.shift();
+
+			this._currentWriter = null;
+		}
+
+		this.fire( 'changesDone' );
+
+		return ret;
+	}
+
+	/**
+	 * {@link module:utils/observablemixin~ObservableMixin#decorate Decorated} function to apply
+	 * {@link module:engine/model/operation/operation~Operation operations} on the model.
+	 *
+	 * @param {module:engine/model/operation/operation~Operation} operation Operation to apply
+	 * @returns {Object} Object with additional information about the applied changes. It properties depends on the
+	 * operation type.
+	 */
+	applyOperation( operation ) {
+		return operation._execute();
+	}
+
+	/**
+	 * Removes all events listeners set by model instance and destroy Document.
+	 */
+	destroy() {
+		this.document.destroy();
+		this.stopListening();
+	}
+
+	/**
+	 * Fired after leaving each {@link module:engine/model/model~Model#enqueueChange} block or outermost
+	 * {@link module:engine/model/model~Model#change} block.
+	 * Have the same parameters as {@link module:engine/model/model~Model#change}.
+	 *
+	 * @event change
+	 */
+
+	/**
+	 * Fired when all queued model changes are done.
+	 *
+	 * @see #change
+	 * @see #enqueueChange
+	 * @event changesDone
+	 */
+
+	/**
+	 * Fired every time any {@link module:engine/model/operation/operation~Operation operation} is applied on the model
+	 * using {@link #applyOperation}.
+	 *
+	 * Note that this is an internal event for the specific use-cases. You can use it if you need to know about each operation
+	 * applied on the document, but in most cases {@link #change} event which is fired when all changes in a
+	 * {@link module:engine/model/batch~Batch} are applied, is a better choice.
+	 *
+	 * With the high priority operation is validated.
+	 *
+	 * With the normal priority operation is executed. After that priority you will be able to get additional
+	 * information about the applied changes returned by {@link module:engine/model/operation/operation~Operation#_execute}
+	 * as `evt.return`.
+	 *
+	 * With the low priority the {@link module:engine/model/document~Document} listen on this event and updates its version.
+	 *
+	 * @event applyOperation
+	 * @param {Array} args Arguments of the `applyOperation` which are an array with a single element:
+	 * {@link module:engine/model/operation/operation~Operation operation}.
+	 */
+}
+
+mix( Model, ObservableMixin );

+ 0 - 8
packages/ckeditor5-engine/src/model/model.jsdoc

@@ -1,8 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/model
- */

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

@@ -17,7 +17,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  *
  * **Note:** If a node is detached from the model tree, you can manipulate it using it's API.
  * However, it is **very important** that nodes already attached to model tree should be only changed through
- * {@link module:engine/model/document~Document#batch Batch API}.
+ * {@link module:engine/model/writer~Writer Writer API}.
  *
  * Changes done by `Node` methods, like {@link module:engine/model/element~Element#insertChildren insertChildren} or
  * {@link module:engine/model/node~Node#setAttribute setAttribute}
@@ -30,9 +30,9 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  * 3. Change `Node` that was already added to the model using `Batch` API.
  *
  * Similarly, you cannot use `Batch` API on a node that has not been added to the model tree, with the exception
- * of {@link module:engine/model/batch~Batch#insert inserting} that node to the model tree.
+ * of {@link module:engine/model/writer~Writer#insert inserting} that node to the model tree.
  *
- * Be aware that using {@link module:engine/model/batch~Batch#remove remove from Batch API} does not allow to use `Node` API because
+ * Be aware that using {@link module:engine/model/writer~Writer#remove remove from Batch API} does not allow to use `Node` API because
  * the information about `Node` is still kept in model document.
  *
  * In case of {@link module:engine/model/element~Element element node}, adding and removing children also counts as changing a node and

+ 1 - 1
packages/ckeditor5-engine/src/model/operation/markeroperation.js

@@ -146,7 +146,7 @@ export default class MarkerOperation extends Operation {
 			json.name,
 			json.oldRange ? Range.fromJSON( json.oldRange, document ) : null,
 			json.newRange ? Range.fromJSON( json.newRange, document ) : null,
-			document.markers,
+			document.model.markers,
 			json.baseVersion
 		);
 	}

+ 1 - 1
packages/ckeditor5-engine/src/model/operation/operation.js

@@ -22,7 +22,7 @@ export default class Operation {
 	constructor( baseVersion ) {
 		/**
 		 * {@link module:engine/model/document~Document#version} on which operation can be applied. If you try to
-		 * {@link module:engine/model/document~Document#applyOperation apply} operation with different base version than the
+		 * {@link module:engine/model/model~Model#applyOperation apply} operation with different base version than the
 		 * {@link module:engine/model/document~Document#version document version} the
 		 * {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown.
 		 *

+ 1 - 1
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -34,7 +34,7 @@ import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
  *
  * Whenever the {@link module:engine/model/document~Document document}
  * has different {@link module:engine/model/document~Document#version}
- * than the operation you want to {@link module:engine/model/document~Document#applyOperation apply}, you need to transform that
+ * than the operation you want to {@link module:engine/model/model~Model#applyOperation apply}, you need to transform that
  * operation by all operations which were already applied to the {@link module:engine/model/document~Document document} and have greater
  * {@link module:engine/model/document~Document#version} than the operation being applied. Transform them in the same order as those
  * operations which were applied. This way all modifications done to the Tree Data Model will be reflected

+ 6 - 6
packages/ckeditor5-engine/src/model/schema.js

@@ -23,7 +23,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  *
  * For instance, if a feature wants to define that an attribute bold is allowed on the text it needs to register this rule like this:
  *
- *		editor.document.schema.allow( '$text', 'bold' );
+ *		editor.model.schema.allow( '$text', 'bold' );
  *
  * Note: items prefixed with `$` are special group of items. By default, `Schema` defines three special items:
  *
@@ -152,7 +152,7 @@ export default class Schema {
 	 *		if ( schema.check( query ) ) { ... }
 	 *
 	 *		// Check whether bold and italic text can be placed at caret position.
-	 *		let caretPos = editor.document.selection.getFirstPosition();
+	 *		let caretPos = editor.model.document.selection.getFirstPosition();
 	 *		let query = {
 	 *			name: '$text',
 	 *			attributes: [ 'bold', 'italic' ],
@@ -418,9 +418,9 @@ export default class Schema {
 	 *
 	 * @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/writer~Writer} writer
 	 */
-	removeDisallowedAttributes( nodes, inside, batch ) {
+	removeDisallowedAttributes( nodes, inside, writer ) {
 		for ( const node of nodes ) {
 			const name = node.is( 'text' ) ? '$text' : node.name;
 			const attributes = Array.from( node.getAttributeKeys() );
@@ -432,13 +432,13 @@ 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 } ) ) {
-						batch.removeAttribute( attribute, node );
+						writer.removeAttribute( attribute, node );
 					}
 				}
 			}
 
 			if ( node.is( 'element' ) ) {
-				this.removeDisallowedAttributes( node.getChildren(), queryPath.concat( node.name ), batch );
+				this.removeDisallowedAttributes( node.getChildren(), queryPath.concat( node.name ), writer );
 			}
 		}
 	}

+ 1 - 1
packages/ckeditor5-engine/src/model/selection.js

@@ -787,7 +787,7 @@ function isUnvisitedBlockContainer( element, visited ) {
 
 	// TODO https://github.com/ckeditor/ckeditor5-engine/issues/532#issuecomment-278900072.
 	// This should not be a `$block` check.
-	return element.document.schema.itemExtends( element.name, '$block' ) && element.parent;
+	return element.document.model.schema.itemExtends( element.name, '$block' ) && element.parent;
 }
 
 // Finds the lowest element in position's ancestors which is a block.

+ 1 - 1
packages/ckeditor5-engine/src/model/text.js

@@ -15,7 +15,7 @@ import Node from './node';
  * **Important:** see {@link module:engine/model/node~Node} to read about restrictions using `Text` and `Node` API.
  *
  * **Note:** keep in mind that `Text` instances might indirectly got removed from model tree when model is changed.
- * This happens when {@linkTODO module:engine/model/writer~writer model writer} is used to change model and the text node is merged with
+ * This happens when {@link module:engine/model/writer~Writer model writer} is used to change model and the text node is merged with
  * another text node. Then, both text nodes are removed and a new text node is inserted into the model. Because of
  * this behavior, keeping references to `Text` is not recommended. Instead, consider creating
  * {@link module:engine/model/liveposition~LivePosition live position} placed before the text node.

+ 1 - 1
packages/ckeditor5-engine/src/model/textproxy.js

@@ -28,7 +28,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  * parameter of methods.
  *
  * **Note:** `TextProxy` is a readonly interface. If you want to perform changes on model data represented by a `TextProxy`
- * use {@linkTODO module:engine/model/writer~writer model writer API}.
+ * use {@link module:engine/model/writer~Writer model writer API}.
  *
  * **Note:** `TextProxy` instances are created on the fly, basing on the current state of model. Because of this, it is
  * highly unrecommended to store references to `TextProxy` instances. `TextProxy` instances are not refreshed when

+ 916 - 0
packages/ckeditor5-engine/src/model/writer.js

@@ -0,0 +1,916 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/writer
+ */
+
+import AttributeDelta from './delta/attributedelta';
+import InsertDelta from './delta/insertdelta';
+import MarkerDelta from './delta/markerdelta';
+import MergeDelta from './delta/mergedelta';
+import MoveDelta from './delta/movedelta';
+import RemoveDelta from './delta/removedelta';
+import RenameDelta from './delta/renamedelta';
+import RootAttributeDelta from './delta/rootattributedelta';
+import SplitDelta from './delta/splitdelta';
+import UnwrapDelta from './delta/unwrapdelta';
+import WeakInsertDelta from './delta/weakinsertdelta';
+import WrapDelta from './delta/wrapdelta';
+
+import AttributeOperation from './operation/attributeoperation';
+import DetachOperation from './operation/detachoperation';
+import InsertOperation from './operation/insertoperation';
+import MarkerOperation from './operation/markeroperation';
+import MoveOperation from './operation/moveoperation';
+import RemoveOperation from './operation/removeoperation';
+import RenameOperation from './operation/renameoperation';
+import RootAttributeOperation from './operation/rootattributeoperation';
+
+import DocumentFragment from './documentfragment';
+import Text from './text';
+import Element from './element';
+import RootElement from './rootelement';
+import Position from './position';
+import Range from './range.js';
+
+import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+/**
+ * Model writer it the proper way of modifying model. It should be used whenever you wants to create node, modify
+ * child nodes, attributes or text. To get writer use {@link module:engine/model/model~Model#change} or
+ * {@link @see module:engine/model/model~Model#enqueueChange}.
+ *
+ *		model.change( writer => {
+ *			writer.insertText( 'foo', paragraph, 'end' );
+ *		} );
+ *
+ * Note that writer can be passed to a nested function but you should never store and use it outside the `change` or
+ * `enqueueChange` block.
+ *
+ * @see module:engine/model/model~Model#change
+ * @see module:engine/model/model~Model#enqueueChange
+ */
+export default class Writer {
+	/**
+	 * Writer class constructor.
+	 *
+	 * It is not recommended to use it directly, use {@link module:engine/model/model~Model#change} or
+	 * {@link module:engine/model/model~Model#enqueueChange} instead.
+	 *
+	 * @protected
+	 * @param {module:engine/model/model~Model} model
+	 * @param {module:engine/model/batch~Batch} batch
+	 */
+	constructor( model, batch ) {
+		/**
+		 * @type {module:engine/model/model~Model}
+		 */
+		this.model = model;
+
+		/**
+		 * @type {module:engine/model/batch~Batch}
+		 */
+		this.batch = batch;
+	}
+
+	/**
+	 * Creates a new {@link module:engine/model/text~Text text node}.
+	 *
+	 *		writer.createText( 'foo' );
+	 *		writer.createText( 'foo', { 'bold': true } );
+	 *
+	 * @param {String} data Text data.
+	 * @param {Object} [attributes] Text attributes.
+	 * @returns {module:engine/model/text~Text} Created text node.
+	 */
+	createText( data, attributes ) {
+		return new Text( data, attributes );
+	}
+
+	/**
+	 * Creates a new {@link module:engine/model/element~Element element}.
+	 *
+	 *		writer.createElement( 'paragraph' );
+	 *		writer.createElement( 'paragraph', { 'alignment': 'center' } );
+	 *
+	 * @param {String} name Name of the element.
+	 * @param {Object} [attributes] Elements attributes.
+	 * @returns {module:engine/model/element~Element} Created element.
+	 */
+	createElement( name, attributes ) {
+		return new Element( name, attributes );
+	}
+
+	/**
+	 * Creates a new {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
+	 *
+	 * @returns {module:engine/model/documentfragment~DocumentFragment} Created document fragment.
+	 */
+	createDocumentFragment() {
+		return new DocumentFragment();
+	}
+
+	/**
+	 * Inserts item on given position.
+	 *
+	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		writer.insert( paragraph, position );
+	 *
+	 * Instead of using position you can use parent and offset:
+	 *
+	 * 		const text = writer.createText( 'foo' );
+	 *		writer.insert( text, paragraph, 5 );
+	 *
+	 * You can also use `end` instead of the offset to insert at the end:
+	 *
+	 * 		const text = writer.createText( 'foo' );
+	 *		writer.insert( text, paragraph, 'end' );
+	 *
+	 * Or insert before or after another element:
+	 *
+	 * 		const paragraph = writer.createElement( 'paragraph' );
+	 *		writer.insert( paragraph, anotherParagraph, 'after' );
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * Note that if the item already has parent it will be removed from the previous parent.
+	 *
+	 * If you want to move {@link module:engine/model/range~Range range} instead of an
+	 * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move move}.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
+	 * item Item or document fragment to insert.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * second parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	insert( item, itemOrPosition, offset ) {
+		const position = Position.createAt( itemOrPosition, offset );
+
+		// For text that has no parent we need to make a WeakInsert.
+		const delta = item instanceof Text && !item.parent ? new WeakInsertDelta() : new InsertDelta();
+
+		// If item has a parent already.
+		if ( item.parent ) {
+			// We need to check if item is going to be inserted within the same document.
+			if ( isSameTree( item.root, position.root ) ) {
+				// If it's we just need to move it.
+				this.move( Range.createOn( item ), position );
+
+				return;
+			}
+			// If it isn't the same root.
+			else {
+				// We need to remove this item from old position first.
+				this.remove( item );
+			}
+		}
+
+		const insert = new InsertOperation( position, item, this.model.document.version );
+
+		this.batch.addDelta( delta );
+		delta.addOperation( insert );
+		this.model.applyOperation( insert );
+
+		// When element is a DocumentFragment we need to move its markers to Document#markers.
+		if ( item instanceof DocumentFragment ) {
+			for ( const [ markerName, markerRange ] of item.markers ) {
+				// We need to migrate marker range from DocumentFragment to Document.
+				const rangeRootPosition = Position.createAt( markerRange.root );
+				const range = new Range(
+					markerRange.start._getCombined( rangeRootPosition, position ),
+					markerRange.end._getCombined( rangeRootPosition, position )
+				);
+
+				this.setMarker( markerName, range );
+			}
+		}
+	}
+
+	/**
+	 * Creates and inserts text on given position. You can optionally set text attributes:
+	 *
+	 *		writer.insertText( 'foo', position );
+	 *		writer.insertText( 'foo', { 'bold': true }, position );
+	 *
+	 * Instead of using position you can use parent and offset or define that text should be inserted at the end
+	 * or before or after other node:
+	 *
+	 * 		writer.insertText( 'foo', paragraph, 5 ); // inserts in paragraph, at offset 5
+	 *		writer.insertText( 'foo', paragraph, 'end' ); // inserts at the end of the paragraph
+	 *		writer.insertText( 'foo', image, 'after' ); // inserts after image
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * @param {String} data Text data.
+	 * @param {Object} [attributes] Text attributes.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * third parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	insertText( text, attributes, itemOrPosition, offset ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
+			this.insert( this.createText( text ), attributes, itemOrPosition );
+		} else {
+			this.insert( this.createText( text, attributes ), itemOrPosition, offset );
+		}
+	}
+
+	/**
+	 * Creates and inserts element on given position. You can optionally set attributes:
+	 *
+	 *		writer.insertElement( 'paragraph', position );
+	 *		writer.insertElement( 'paragraph', { 'alignment': 'center' }, position );
+	 *
+	 * Instead of using position you can use parent and offset or define that text should be inserted at the end
+	 * or before or after other node:
+	 *
+	 * 		writer.insertElement( 'paragraph', paragraph, 5 ); // inserts in paragraph, at offset 5
+	 *		writer.insertElement( 'paragraph', blockquote, 'end' ); // insets at the end of the blockquote
+	 *		writer.insertElement( 'paragraph', image, 'after' ); // inserts after image
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * @param {String} name Name of the element.
+	 * @param {Object} [attributes] Elements attributes.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * third parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	insertElement( name, attributes, itemOrPosition, offset ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
+			this.insert( this.createElement( name ), attributes, itemOrPosition );
+		} else {
+			this.insert( this.createElement( name, attributes ), itemOrPosition, offset );
+		}
+	}
+
+	/**
+	 * Inserts item at the end of the given parent.
+	 *
+	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		writer.append( paragraph, root );
+	 *
+	 * Note that if the item already has parent it will be removed from the previous parent.
+	 *
+	 * If you want to move {@link module:engine/model/range~Range range} instead of an
+	 * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move move}.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
+	 * item Item or document fragment to insert.
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
+	 */
+	append( item, parent ) {
+		this.insert( item, parent, 'end' );
+	}
+
+	/**
+	 * Creates text node and inserts it at the end of the parent. You can optionally set text attributes:
+	 *
+	 *		writer.appendText( 'foo', paragraph );
+	 *		writer.appendText( 'foo', { 'bold': true }, paragraph );
+	 *
+	 * @param {String} text Text data.
+	 * @param {Object} [attributes] Text attributes.
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
+	 */
+	appendText( text, attributes, parent ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
+			this.insert( this.createText( text ), attributes, 'end' );
+		} else {
+			this.insert( this.createText( text, attributes ), parent, 'end' );
+		}
+	}
+
+	/**
+	 * Creates element and inserts it at the end of the parent. You can optionally set attributes:
+	 *
+	 *		writer.appendElement( 'paragraph', root );
+	 *		writer.appendElement( 'paragraph', { 'alignment': 'center' }, root );
+	 *
+	 * @param {String} name Name of the element.
+	 * @param {Object} [attributes] Elements attributes.
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
+	 */
+	appendElement( name, attributes, parent ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
+			this.insert( this.createElement( name ), attributes, 'end' );
+		} else {
+			this.insert( this.createElement( name, attributes ), parent, 'end' );
+		}
+	}
+
+	/**
+	 * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
+	 * or on a {@link module:engine/model/range~Range range}.
+	 *
+	 * @param {String} key Attribute key.
+	 * @param {*} value Attribute new value.
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range on which the attribute will be set.
+	 */
+	setAttribute( key, value, itemOrRange ) {
+		if ( itemOrRange instanceof Range ) {
+			setAttributeToRange( this, key, value, itemOrRange );
+		} else {
+			setAttributeToItem( this, key, value, itemOrRange );
+		}
+	}
+
+	/**
+	 * Sets values of attributes on a {@link module:engine/model/item~Item model item}
+	 * or on a {@link module:engine/model/range~Range range}.
+	 *
+	 *		writer.setAttributes( {
+	 *			'bold': true,
+	 *			'italic': true
+	 *		}, range );
+	 *
+	 * @param {Object} attributes Attributes keys and values.
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range on which the attributes will be set.
+	 */
+	setAttributes( attributes, itemOrRange ) {
+		for ( const [ key, val ] of toMap( attributes ) ) {
+			this.setAttribute( key, val, itemOrRange );
+		}
+	}
+
+	/**
+	 * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
+	 * or from a {@link module:engine/model/range~Range range}.
+	 *
+	 * @param {String} key Attribute key.
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range from which the attribute will be removed.
+	 */
+	removeAttribute( key, itemOrRange ) {
+		if ( itemOrRange instanceof Range ) {
+			setAttributeToRange( this, key, null, itemOrRange );
+		} else {
+			setAttributeToItem( this, key, null, itemOrRange );
+		}
+	}
+
+	/**
+	 * Removes all attributes from all elements in the range or from the given item.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range from which all attributes will be removed.
+	 */
+	clearAttributes( itemOrRange ) {
+		const removeAttributesFromItem = item => {
+			for ( const attribute of item.getAttributeKeys() ) {
+				this.removeAttribute( attribute, item );
+			}
+		};
+
+		if ( !( itemOrRange instanceof Range ) ) {
+			removeAttributesFromItem( itemOrRange );
+		} else {
+			for ( const item of itemOrRange.getItems() ) {
+				removeAttributesFromItem( item );
+			}
+		}
+	}
+
+	/**
+	 * Moves all items in the source range to the target position.
+	 *
+	 *		writer.move( sourceRange, targetPosition );
+	 *
+	 * Instead of the target position you can use parent and offset or define that range should be moved to the end
+	 * or before or after chosen item:
+	 *
+	 * 		writer.move( sourceRange, paragraph, 5 ); // moves all items in the range to the paragraph at offset 5
+	 *		writer.move( sourceRange, blockquote, 'end' ); // moves all items in the range at the end of the blockquote
+	 *		writer.move( sourceRange, image, 'after' ); // moves all items in the range after the image
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * Note that items can be moved only within the same tree. It means that you can move items within the same root
+	 * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
+	 * but you can not move items from document fragment to the document or from one detached element to another. Use
+	 * {@link module:engine/model/writer~Writer#insert} in such cases.
+	 *
+	 * @param {module:engine/model/range~Range} range Source range.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * second parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	move( range, itemOrPosition, offset ) {
+		if ( !( range instanceof Range ) ) {
+			/**
+			 * Invalid range to move.
+			 *
+			 * @error writer-move-invalid-range
+			 */
+			throw new CKEditorError( 'writer-move-invalid-range: Invalid range to move.' );
+		}
+
+		if ( !range.isFlat ) {
+			/**
+			 * Range to move is not flat.
+			 *
+			 * @error writer-move-range-not-flat
+			 */
+			throw new CKEditorError( 'writer-move-range-not-flat: Range to move is not flat.' );
+		}
+
+		const position = Position.createAt( itemOrPosition, offset );
+
+		if ( !isSameTree( range.root, position.root ) ) {
+			/**
+			 * Range is going to be moved within not the same document. Please use
+			 * {@link module:engine/model/writer~Writer#insert insert} instead.
+			 *
+			 * @error writer-move-different-document
+			 */
+			throw new CKEditorError( 'writer-move-different-document: Range is going to be moved between different documents.' );
+		}
+
+		const delta = new MoveDelta();
+		this.batch.addDelta( delta );
+
+		const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, this.model.document.version );
+		delta.addOperation( operation );
+		this.model.applyOperation( operation );
+	}
+
+	/**
+	 * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
+	 */
+	remove( itemOrRange ) {
+		const addRemoveDelta = ( position, howMany ) => {
+			const delta = new RemoveDelta();
+			this.batch.addDelta( delta );
+			let operation;
+
+			if ( position.root.document ) {
+				const graveyard = this.model.document.graveyard;
+				const gyPosition = new Position( graveyard, [ 0 ] );
+
+				operation = new RemoveOperation( position, howMany, gyPosition, this.model.document.version );
+			} else {
+				operation = new DetachOperation( position, howMany, this.model.document.version );
+			}
+
+			delta.addOperation( operation );
+			this.model.applyOperation( operation );
+		};
+
+		if ( itemOrRange instanceof Range ) {
+			// The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
+			const ranges = itemOrRange.getMinimalFlatRanges().reverse();
+
+			for ( const flat of ranges ) {
+				addRemoveDelta( flat.start, flat.end.offset - flat.start.offset );
+			}
+		} else {
+			const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
+
+			addRemoveDelta( Position.createBefore( itemOrRange ), howMany );
+		}
+	}
+
+	/**
+	 * Merges two siblings at the given position.
+	 *
+	 * Node before and after the position have to be an element. Otherwise `writer-merge-no-element-before` or
+	 * `writer-merge-no-element-after` error will be thrown.
+	 *
+	 * @param {module:engine/model/position~Position} position Position of merge.
+	 */
+	merge( position ) {
+		const delta = new MergeDelta();
+		this.batch.addDelta( delta );
+
+		const nodeBefore = position.nodeBefore;
+		const nodeAfter = position.nodeAfter;
+
+		if ( !( nodeBefore instanceof Element ) ) {
+			/**
+			 * Node before merge position must be an element.
+			 *
+			 * @error writer-merge-no-element-before
+			 */
+			throw new CKEditorError( 'writer-merge-no-element-before: Node before merge position must be an element.' );
+		}
+
+		if ( !( nodeAfter instanceof Element ) ) {
+			/**
+			 * Node after merge position must be an element.
+			 *
+			 * @error writer-merge-no-element-after
+			 */
+			throw new CKEditorError( 'writer-merge-no-element-after: Node after merge position must be an element.' );
+		}
+
+		const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
+		const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset );
+
+		const move = new MoveOperation(
+			positionAfter,
+			nodeAfter.maxOffset,
+			positionBefore,
+			this.model.document.version
+		);
+
+		move.isSticky = true;
+		delta.addOperation( move );
+		this.model.applyOperation( move );
+
+		const graveyard = this.model.document.graveyard;
+		const gyPosition = new Position( graveyard, [ 0 ] );
+
+		const remove = new RemoveOperation( position, 1, gyPosition, this.model.document.version );
+		delta.addOperation( remove );
+		this.model.applyOperation( remove );
+	}
+
+	/**
+	 * Renames given element.
+	 *
+	 * @param {module:engine/model/element~Element} element The element to rename.
+	 * @param {String} newName New element name.
+	 */
+	rename( element, newName ) {
+		if ( !( element instanceof Element ) ) {
+			/**
+			 * Trying to rename an object which is not an instance of Element.
+			 *
+			 * @error writer-rename-not-element-instance
+			 */
+			throw new CKEditorError(
+				'writer-rename-not-element-instance: Trying to rename an object which is not an instance of Element.'
+			);
+		}
+
+		const delta = new RenameDelta();
+		this.batch.addDelta( delta );
+
+		const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, this.model.document.version );
+		delta.addOperation( renameOperation );
+		this.model.applyOperation( renameOperation );
+	}
+
+	/**
+	 * Splits an element at the given position.
+	 *
+	 * 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.
+	 */
+	split( position ) {
+		const delta = new SplitDelta();
+		this.batch.addDelta( delta );
+
+		const splitElement = position.parent;
+
+		if ( !splitElement.parent ) {
+			/**
+			 * Element with no parent can not be split.
+			 *
+			 * @error writer-split-element-no-parent
+			 */
+			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 insert = new InsertOperation(
+			Position.createAfter( splitElement ),
+			copy,
+			this.model.document.version
+		);
+
+		delta.addOperation( insert );
+		this.model.applyOperation( insert );
+
+		const move = new MoveOperation(
+			position,
+			splitElement.maxOffset - position.offset,
+			Position.createFromParentAndOffset( copy, 0 ),
+			this.model.document.version
+		);
+		move.isSticky = true;
+
+		delta.addOperation( move );
+		this.model.applyOperation( move );
+	}
+
+	/**
+	 * Wraps given range with given element or with a new element with specified name, if string has been passed.
+	 *
+	 * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat}). If not, error will be thrown.
+	 *
+	 * @param {module:engine/model/range~Range} range Range to wrap.
+	 * @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with.
+	 */
+	wrap( range, elementOrString ) {
+		if ( !range.isFlat ) {
+			/**
+			 * Range to wrap is not flat.
+			 *
+			 * @error writer-wrap-range-not-flat
+			 */
+			throw new CKEditorError( 'writer-wrap-range-not-flat: Range to wrap is not flat.' );
+		}
+
+		const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
+
+		if ( element.childCount > 0 ) {
+			/**
+			 * Element to wrap with is not empty.
+			 *
+			 * @error writer-wrap-element-not-empty
+			 */
+			throw new CKEditorError( 'writer-wrap-element-not-empty: Element to wrap with is not empty.' );
+		}
+
+		if ( element.parent !== null ) {
+			/**
+			 * Element to wrap with is already attached to a tree model.
+			 *
+			 * @error writer-wrap-element-attached
+			 */
+			throw new CKEditorError( 'writer-wrap-element-attached: Element to wrap with is already attached to tree model.' );
+		}
+
+		const delta = new WrapDelta();
+		this.batch.addDelta( delta );
+
+		const insert = new InsertOperation( range.end, element, this.model.document.version );
+		delta.addOperation( insert );
+		this.model.applyOperation( insert );
+
+		const targetPosition = Position.createFromParentAndOffset( element, 0 );
+		const move = new MoveOperation(
+			range.start,
+			range.end.offset - range.start.offset,
+			targetPosition,
+			this.model.document.version
+		);
+		delta.addOperation( move );
+		this.model.applyOperation( move );
+	}
+
+	/**
+	 * Unwraps children of the given element – all its children are moved before it and then the element is removed.
+	 * Throws error if you try to unwrap an element which does not have a parent.
+	 *
+	 * @param {module:engine/model/element~Element} element Element to unwrap.
+	 */
+	unwrap( element ) {
+		if ( element.parent === null ) {
+			/**
+			 * Trying to unwrap an element which has no parent.
+			 *
+			 * @error writer-unwrap-element-no-parent
+			 */
+			throw new CKEditorError( 'writer-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
+		}
+
+		const delta = new UnwrapDelta();
+		this.batch.addDelta( delta );
+
+		const sourcePosition = Position.createFromParentAndOffset( element, 0 );
+
+		const move = new MoveOperation(
+			sourcePosition,
+			element.maxOffset,
+			Position.createBefore( element ),
+			this.model.document.version
+		);
+
+		move.isSticky = true;
+		delta.addOperation( move );
+		this.model.applyOperation( move );
+
+		// Computing new position because we moved some nodes before `element`.
+		// If we would cache `Position.createBefore( element )` we remove wrong node.
+		const graveyard = this.model.document.graveyard;
+		const gyPosition = new Position( graveyard, [ 0 ] );
+
+		const remove = new RemoveOperation( Position.createBefore( element ), 1, gyPosition, this.model.document.version );
+		delta.addOperation( remove );
+		this.model.applyOperation( remove );
+	}
+
+	/**
+	 * Adds or updates {@link module:engine/model/markercollection~Marker marker} with given name to given `range`.
+	 *
+	 * If passed name is a name of already existing marker (or {@link module:engine/model/markercollection~Marker Marker} instance
+	 * is passed), `range` parameter may be omitted. In this case marker will not be updated in
+	 * {@link module:engine/model/model~Model#markers document marker collection}. However the marker will be added to
+	 * the document history. This may be important for other features, like undo. From document history point of view, it will
+	 * look like the marker was created and added to the document at the moment when it is set using this method.
+	 *
+	 * This is useful if the marker is created before it can be added to document history (e.g. a feature creating the marker
+	 * is waiting for additional data, etc.). In this case, the marker may be first created directly through
+	 * {@link module:engine/model/markercollection~MarkerCollection MarkerCollection API} and only later added using `Batch` API.
+	 *
+	 * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to add or update.
+	 * @param {module:engine/model/range~Range} [newRange] Marker range.
+	 */
+	setMarker( markerOrName, newRange ) {
+		const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
+		const currentMarker = this.model.markers.get( name );
+
+		if ( !newRange && !currentMarker ) {
+			/**
+			 * Range parameter is required when adding a new marker.
+			 *
+			 * @error writer-setMarker-no-range
+			 */
+			throw new CKEditorError( 'writer-setMarker-no-range: Range parameter is required when adding a new marker.' );
+		}
+
+		const currentRange = currentMarker ? currentMarker.getRange() : null;
+
+		if ( !newRange ) {
+			// If `newRange` is not given, treat this as synchronizing existing marker.
+			// Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
+			addMarkerOperation( this, name, null, currentRange );
+		} else {
+			// Just change marker range.
+			addMarkerOperation( this, name, currentRange, newRange );
+		}
+	}
+
+	/**
+	 * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
+	 *
+	 * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
+	 */
+	removeMarker( markerOrName ) {
+		const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
+
+		if ( !this.model.markers.has( name ) ) {
+			/**
+			 * Trying to remove marker which does not exist.
+			 *
+			 * @error writer-removeMarker-no-marker
+			 */
+			throw new CKEditorError( 'writer-removeMarker-no-marker: Trying to remove marker which does not exist.' );
+		}
+
+		const oldRange = this.model.markers.get( name ).getRange();
+
+		addMarkerOperation( this, name, oldRange, null );
+	}
+}
+
+// Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
+//
+// Because attribute operation needs to have the same attribute value on the whole range, this function splits
+// the range into smaller parts.
+//
+// @private
+// @param {module:engine/model/writer~Writer} writer
+// @param {String} key Attribute key.
+// @param {*} value Attribute new value.
+// @param {module:engine/model/range~Range} range Model range on which the attribute will be set.
+function setAttributeToRange( writer, key, value, range ) {
+	const delta = new AttributeDelta();
+	const model = writer.model;
+	const doc = model.document;
+
+	// Position of the last split, the beginning of the new range.
+	let lastSplitPosition = range.start;
+
+	// Currently position in the scanning range. Because we need value after the position, it is not a current
+	// position of the iterator but the previous one (we need to iterate one more time to get the value after).
+	let position;
+
+	// Value before the currently position.
+	let valueBefore;
+
+	// Value after the currently position.
+	let valueAfter;
+
+	for ( const val of range ) {
+		valueAfter = val.item.getAttribute( key );
+
+		// At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
+		// because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
+		if ( position && valueBefore != valueAfter ) {
+			// if valueBefore == value there is nothing to change, so we add operation only if these values are different.
+			if ( valueBefore != value ) {
+				addOperation();
+			}
+
+			lastSplitPosition = position;
+		}
+
+		position = val.nextPosition;
+		valueBefore = valueAfter;
+	}
+
+	// Because position in the loop is not the iterator position (see let position comment), the last position in
+	// the while loop will be last but one position in the range. We need to check the last position manually.
+	if ( position instanceof Position && position != lastSplitPosition && valueBefore != value ) {
+		addOperation();
+	}
+
+	function addOperation() {
+		// Add delta to the batch only if there is at least operation in the delta. Add delta only once.
+		if ( delta.operations.length === 0 ) {
+			writer.batch.addDelta( delta );
+		}
+
+		const range = new Range( lastSplitPosition, position );
+		const operation = new AttributeOperation( range, key, valueBefore, value, doc.version );
+
+		delta.addOperation( operation );
+		model.applyOperation( operation );
+	}
+}
+
+// Sets given attribute to the given node. When attribute value is null then attribute will be removed.
+//
+// @private
+// @param {module:engine/model/writer~Writer} writer
+// @param {String} key Attribute key.
+// @param {*} value Attribute new value.
+// @param {module:engine/model/item~Item} item Model item on which the attribute will be set.
+function setAttributeToItem( writer, key, value, item ) {
+	const model = writer.model;
+	const doc = model.document;
+	const previousValue = item.getAttribute( key );
+	let range, operation;
+
+	if ( previousValue != value ) {
+		const delta = item.root === item ? new RootAttributeDelta() : new AttributeDelta();
+		writer.batch.addDelta( delta );
+
+		if ( item.root === item ) {
+			// If we change attributes of root element, we have to use `RootAttributeOperation`.
+			operation = new RootAttributeOperation( item, key, previousValue, value, doc.version );
+		} else {
+			if ( item.is( 'element' ) ) {
+				// If we change the attribute of the element, we do not want to change attributes of its children, so
+				// the end of the range cannot be after the closing tag, it should be inside that element, before any of
+				// it's children, so the range will contain only the opening tag.
+				range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
+			} else {
+				// If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
+				// all characters represented by it.
+				range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
+			}
+
+			operation = new AttributeOperation( range, key, previousValue, value, doc.version );
+		}
+
+		delta.addOperation( operation );
+		model.applyOperation( operation );
+	}
+}
+
+// Creates and adds marker operation to {@link module:engine/model/delta/delta~Delta delta}.
+//
+// @private
+// @param {module:engine/model/writer~Writer} writer
+// @param {String} name Marker name.
+// @param {module:engine/model/range~Range} oldRange Marker range before the change.
+// @param {module:engine/model/range~Range} newRange Marker range after the change.
+function addMarkerOperation( writer, name, oldRange, newRange ) {
+	const model = writer.model;
+	const doc = model.document;
+	const delta = new MarkerDelta();
+
+	const operation = new MarkerOperation( name, oldRange, newRange, model.markers, doc.version );
+
+	writer.batch.addDelta( delta );
+	delta.addOperation( operation );
+	model.applyOperation( operation );
+}
+
+// Returns `true` if both root elements are the same element or both are documents root elements.
+//
+// Elements in the same tree can be moved (for instance you can move element form one documents root to another, or
+// within the same document fragment), but when element supposed to be moved from document fragment to the document, or
+// to another document it should be removed and inserted to avoid problems with OT. This is because features like undo or
+// collaboration may track changes on the document but ignore changes on detached fragments and should not get
+// unexpected `move` operation.
+function isSameTree( rootA, rootB ) {
+	// If it is the same root this is the same tree.
+	if ( rootA === rootB ) {
+		return true;
+	}
+
+	// If both roots are documents root it is operation within the document what we still treat as the same tree.
+	if ( rootA instanceof RootElement && rootB instanceof RootElement ) {
+		return true;
+	}
+
+	return false;
+}

+ 3 - 1
packages/ckeditor5-engine/src/view/domconverter.js

@@ -1061,7 +1061,9 @@ export default class DomConverter {
 
 		const direction = getNext ? 'nextNode' : 'previousNode';
 		const document = node.ownerDocument;
-		const treeWalker = document.createTreeWalker( document.childNodes[ 0 ], NodeFilter.SHOW_TEXT );
+		const topmostParent = getAncestors( node )[ 0 ];
+
+		const treeWalker = document.createTreeWalker( topmostParent, NodeFilter.SHOW_TEXT );
 
 		treeWalker.currentNode = node;
 

+ 2 - 1
packages/ckeditor5-engine/src/view/observer/focusobserver.js

@@ -35,7 +35,8 @@ export default class FocusObserver extends DomEventObserver {
 			// We need to wait until `SelectionObserver` handle the event and then render. Otherwise rendering will
 			// overwrite new DOM selection with selection from the view.
 			// See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
-			this._renderTimeoutId = setTimeout( () => document.render(), 0 );
+			// Long timeout is needed to solve #676 and https://github.com/ckeditor/ckeditor5-engine/issues/1157 issues.
+			this._renderTimeoutId = setTimeout( () => document.render(), 50 );
 		} );
 
 		document.on( 'blur', ( evt, data ) => {

+ 122 - 98
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import ModelDocument from '../../src/model/document';
+import Model from '../../src/model/model';
 import DataController from '../../src/controller/datacontroller';
 import HtmlDataProcessor from '../../src/dataprocessor/htmldataprocessor';
 
@@ -23,35 +23,36 @@ import { parse as parseView } from '../../src/dev-utils/view';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 
 describe( 'DataController', () => {
-	let modelDocument, htmlDataProcessor, data, schema;
+	let model, modelDocument, htmlDataProcessor, data, schema;
 
 	beforeEach( () => {
-		modelDocument = new ModelDocument();
+		model = new Model();
+		modelDocument = model.document;
 		modelDocument.createRoot();
 		modelDocument.createRoot( '$root', 'title' );
 
 		htmlDataProcessor = new HtmlDataProcessor();
 
-		data = new DataController( modelDocument, htmlDataProcessor );
+		data = new DataController( model, htmlDataProcessor );
 
-		schema = modelDocument.schema;
+		schema = model.schema;
 	} );
 
 	describe( 'constructor()', () => {
 		it( 'works without data processor', () => {
-			const data = new DataController( modelDocument );
+			const data = new DataController( model );
 
 			expect( data.processor ).to.be.undefined;
 		} );
 	} );
 
-	describe( 'parse', () => {
+	describe( 'parse()', () => {
 		it( 'should set text', () => {
 			schema.allow( { name: '$text', inside: '$root' } );
-			const model = data.parse( '<p>foo<b>bar</b></p>', modelDocument.batch() );
+			const output = data.parse( '<p>foo<b>bar</b></p>' );
 
-			expect( model ).to.instanceof( ModelDocumentFragment );
-			expect( stringify( model ) ).to.equal( 'foobar' );
+			expect( output ).to.instanceof( ModelDocumentFragment );
+			expect( stringify( output ) ).to.equal( 'foobar' );
 		} );
 
 		it( 'should set paragraph', () => {
@@ -59,10 +60,10 @@ describe( 'DataController', () => {
 
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 
-			const model = data.parse( '<p>foo<b>bar</b></p>', modelDocument.batch() );
+			const output = data.parse( '<p>foo<b>bar</b></p>' );
 
-			expect( model ).to.instanceof( ModelDocumentFragment );
-			expect( stringify( model ) ).to.equal( '<paragraph>foobar</paragraph>' );
+			expect( output ).to.instanceof( ModelDocumentFragment );
+			expect( stringify( output ) ).to.equal( '<paragraph>foobar</paragraph>' );
 		} );
 
 		it( 'should set two paragraphs', () => {
@@ -70,10 +71,10 @@ describe( 'DataController', () => {
 
 			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
 
-			const model = data.parse( '<p>foo</p><p>bar</p>', modelDocument.batch() );
+			const output = data.parse( '<p>foo</p><p>bar</p>' );
 
-			expect( model ).to.instanceof( ModelDocumentFragment );
-			expect( stringify( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
+			expect( output ).to.instanceof( ModelDocumentFragment );
+			expect( stringify( output ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
 		} );
 
 		it( 'should set paragraphs with bold', () => {
@@ -83,26 +84,26 @@ 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>', modelDocument.batch() );
+			const output = data.parse( '<p>foo<b>bar</b></p>' );
 
-			expect( model ).to.instanceof( ModelDocumentFragment );
-			expect( stringify( model ) ).to.equal( '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
+			expect( output ).to.instanceof( ModelDocumentFragment );
+			expect( stringify( output ) ).to.equal( '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 		} );
 
 		it( 'should parse in the root context by default', () => {
-			const model = data.parse( 'foo', modelDocument.batch() );
+			const output = data.parse( 'foo' );
 
-			expect( stringify( model ) ).to.equal( '' );
+			expect( stringify( output ) ).to.equal( '' );
 		} );
 
 		it( 'should accept parsing context', () => {
-			const model = data.parse( 'foo', modelDocument.batch(), '$block' );
+			const output = data.parse( 'foo', '$block' );
 
-			expect( stringify( model ) ).to.equal( 'foo' );
+			expect( stringify( output ) ).to.equal( 'foo' );
 		} );
 	} );
 
-	describe( 'toModel', () => {
+	describe( 'toModel()', () => {
 		beforeEach( () => {
 			schema.registerItem( 'paragraph', '$block' );
 
@@ -111,18 +112,18 @@ describe( 'DataController', () => {
 
 		it( 'should convert content of an element #1', () => {
 			const viewElement = parseView( '<p>foo</p>' );
-			const model = data.toModel( viewElement, modelDocument.batch() );
+			const output = data.toModel( viewElement );
 
-			expect( model ).to.instanceof( ModelDocumentFragment );
-			expect( stringify( model ) ).to.equal( '<paragraph>foo</paragraph>' );
+			expect( output ).to.instanceof( ModelDocumentFragment );
+			expect( stringify( output ) ).to.equal( '<paragraph>foo</paragraph>' );
 		} );
 
 		it( 'should convert content of an element #2', () => {
 			const viewFragment = parseView( '<p>foo</p><p>bar</p>' );
-			const model = data.toModel( viewFragment, modelDocument.batch() );
+			const output = data.toModel( viewFragment );
 
-			expect( model ).to.be.instanceOf( ModelDocumentFragment );
-			expect( stringify( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
+			expect( output ).to.be.instanceOf( ModelDocumentFragment );
+			expect( stringify( output ) ).to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
 		} );
 
 		it( 'should accept parsing context', () => {
@@ -134,19 +135,19 @@ describe( 'DataController', () => {
 			const viewFragment = new ViewDocumentFragment( [ parseView( 'foo' ) ] );
 
 			// Model fragment in root.
-			expect( stringify( data.toModel( viewFragment, modelDocument.batch() ) ) ).to.equal( '' );
+			expect( stringify( data.toModel( viewFragment ) ) ).to.equal( '' );
 
 			// Model fragment in inline root.
-			expect( stringify( data.toModel( viewFragment, modelDocument.batch(), 'inlineRoot' ) ) ).to.equal( 'foo' );
+			expect( stringify( data.toModel( viewFragment, 'inlineRoot' ) ) ).to.equal( 'foo' );
 		} );
 	} );
 
-	describe( 'set', () => {
+	describe( 'set()', () => {
 		it( 'should set data to root', () => {
 			schema.allow( { name: '$text', inside: '$root' } );
 			data.set( 'foo' );
 
-			expect( getData( modelDocument, { withoutSelection: true } ) ).to.equal( 'foo' );
+			expect( getData( model, { withoutSelection: true } ) ).to.equal( 'foo' );
 		} );
 
 		it( 'should create a batch', () => {
@@ -172,8 +173,8 @@ describe( 'DataController', () => {
 			data.set( 'foo', 'main' );
 			data.set( 'Bar', 'title' );
 
-			expect( getData( modelDocument, { withoutSelection: true, rootName: 'main' } ) ).to.equal( 'foo' );
-			expect( getData( modelDocument, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'Bar' );
+			expect( getData( model, { withoutSelection: true, rootName: 'main' } ) ).to.equal( 'foo' );
+			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'Bar' );
 
 			expect( count( modelDocument.history.getDeltas() ) ).to.equal( 2 );
 		} );
@@ -185,18 +186,18 @@ describe( 'DataController', () => {
 
 			data.set( 'foo', 'title' );
 
-			expect( getData( modelDocument, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'foo' );
+			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'foo' );
 
 			data.set( '', 'title' );
 
-			expect( getData( modelDocument, { withoutSelection: true, rootName: 'title' } ) ).to.equal( '' );
+			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( '' );
 		} );
 	} );
 
-	describe( 'get', () => {
+	describe( 'get()', () => {
 		it( 'should get paragraph with text', () => {
-			modelDocument.schema.registerItem( 'paragraph', '$block' );
-			setData( modelDocument, '<paragraph>foo</paragraph>' );
+			schema.registerItem( 'paragraph', '$block' );
+			setData( model, '<paragraph>foo</paragraph>' );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 
@@ -204,8 +205,8 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should get empty paragraph', () => {
-			modelDocument.schema.registerItem( 'paragraph', '$block' );
-			setData( modelDocument, '<paragraph></paragraph>' );
+			schema.registerItem( 'paragraph', '$block' );
+			setData( model, '<paragraph></paragraph>' );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 
@@ -213,8 +214,8 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should get two paragraphs', () => {
-			modelDocument.schema.registerItem( 'paragraph', '$block' );
-			setData( modelDocument, '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
+			schema.registerItem( 'paragraph', '$block' );
+			setData( model, '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 
@@ -222,15 +223,15 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should get text directly in root', () => {
-			modelDocument.schema.allow( { name: '$text', inside: '$root' } );
-			setData( modelDocument, 'foo' );
+			schema.allow( { name: '$text', inside: '$root' } );
+			setData( model, 'foo' );
 
 			expect( data.get() ).to.equal( 'foo' );
 		} );
 
 		it( 'should get paragraphs without bold', () => {
-			modelDocument.schema.registerItem( 'paragraph', '$block' );
-			setData( modelDocument, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
+			schema.registerItem( 'paragraph', '$block' );
+			setData( model, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 
@@ -238,8 +239,8 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should get paragraphs with bold', () => {
-			modelDocument.schema.registerItem( 'paragraph', '$block' );
-			setData( modelDocument, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
+			schema.registerItem( 'paragraph', '$block' );
+			setData( model, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 			buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
@@ -248,11 +249,11 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should get root name as a parameter', () => {
-			modelDocument.schema.registerItem( 'paragraph', '$block' );
-			modelDocument.schema.allow( { name: '$text', inside: '$root' } );
+			schema.registerItem( 'paragraph', '$block' );
+			schema.allow( { name: '$text', inside: '$root' } );
 
-			setData( modelDocument, '<paragraph>foo</paragraph>', { rootName: 'main' } );
-			setData( modelDocument, 'Bar', { rootName: 'title' } );
+			setData( model, '<paragraph>foo</paragraph>', { rootName: 'main' } );
+			setData( model, 'Bar', { rootName: 'title' } );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 			buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
@@ -263,47 +264,43 @@ describe( 'DataController', () => {
 		} );
 	} );
 
-	describe( 'stringify', () => {
-		let batch;
-
+	describe( 'stringify()', () => {
 		beforeEach( () => {
-			modelDocument.schema.registerItem( 'paragraph', '$block' );
-			modelDocument.schema.registerItem( 'div' );
+			schema.registerItem( 'paragraph', '$block' );
+			schema.registerItem( 'div' );
 
-			modelDocument.schema.allow( { name: '$block', inside: 'div' } );
-			modelDocument.schema.allow( { name: 'div', inside: '$root' } );
+			schema.allow( { name: '$block', inside: 'div' } );
+			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, batch );
+			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', schema );
 
 			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, batch );
+			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', schema );
 
 			expect( data.stringify( modelDocumentFragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
 		} );
 	} );
 
-	describe( 'toView', () => {
+	describe( 'toView()', () => {
 		beforeEach( () => {
-			modelDocument.schema.registerItem( 'paragraph', '$block' );
-			modelDocument.schema.registerItem( 'div' );
+			schema.registerItem( 'paragraph', '$block' );
+			schema.registerItem( 'div' );
 
-			modelDocument.schema.allow( { name: '$block', inside: 'div' } );
-			modelDocument.schema.allow( { name: 'div', inside: '$root' } );
+			schema.allow( { name: '$block', inside: 'div' } );
+			schema.allow( { name: 'div', inside: '$root' } );
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 		} );
 
 		it( 'should convert a content of an element', () => {
-			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', modelDocument.schema, modelDocument.batch() );
+			const modelElement = parseModel( '<div><paragraph>foo</paragraph></div>', schema );
 
 			const viewDocumentFragment = data.toView( modelElement );
 
@@ -317,12 +314,7 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should convert a document fragment', () => {
-			const modelDocumentFragment = parseModel(
-				'<paragraph>foo</paragraph><paragraph>bar</paragraph>',
-				modelDocument.schema,
-				modelDocument.batch()
-			);
-
+			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', schema );
 			const viewDocumentFragment = data.toView( modelDocumentFragment );
 
 			expect( viewDocumentFragment ).to.be.instanceOf( ViewDocumentFragment );
@@ -336,7 +328,7 @@ describe( 'DataController', () => {
 		} );
 	} );
 
-	describe( 'destroy', () => {
+	describe( 'destroy()', () => {
 		it( 'should be there for you', () => {
 			// Should not throw.
 			data.destroy();
@@ -345,7 +337,7 @@ describe( 'DataController', () => {
 		} );
 	} );
 
-	describe( 'insertContent', () => {
+	describe( 'insertContent()', () => {
 		it( 'should be decorated', () => {
 			schema.allow( { name: '$text', inside: '$root' } ); // To surpress warnings.
 
@@ -361,25 +353,35 @@ describe( 'DataController', () => {
 		it( 'should insert content (item)', () => {
 			schema.registerItem( 'paragraph', '$block' );
 
-			setData( modelDocument, '<paragraph>fo[]ar</paragraph>' );
+			setData( model, '<paragraph>fo[]ar</paragraph>' );
 
 			data.insertContent( new ModelText( 'ob' ), modelDocument.selection );
 
-			expect( getData( modelDocument ) ).to.equal( '<paragraph>foob[]ar</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>foob[]ar</paragraph>' );
 		} );
 
 		it( 'should insert content (document fragment)', () => {
 			schema.registerItem( 'paragraph', '$block' );
 
-			setData( modelDocument, '<paragraph>fo[]ar</paragraph>' );
+			setData( model, '<paragraph>fo[]ar</paragraph>' );
 
 			data.insertContent( new ModelDocumentFragment( [ new ModelText( 'ob' ) ] ), modelDocument.selection );
 
-			expect( getData( modelDocument ) ).to.equal( '<paragraph>foob[]ar</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>foob[]ar</paragraph>' );
+		} );
+
+		it( 'should use parent batch', () => {
+			schema.registerItem( 'paragraph', '$block' );
+			setData( model, '<paragraph>[]</paragraph>' );
+
+			model.change( writer => {
+				data.insertContent( new ModelText( 'abc' ), modelDocument.selection );
+				expect( writer.batch.deltas ).to.length( 1 );
+			} );
 		} );
 	} );
 
-	describe( 'deleteContent', () => {
+	describe( 'deleteContent()', () => {
 		it( 'should be decorated', () => {
 			const spy = sinon.spy();
 
@@ -393,18 +395,29 @@ describe( 'DataController', () => {
 		it( 'should delete selected content', () => {
 			schema.registerItem( 'paragraph', '$block' );
 
-			setData( modelDocument, '<paragraph>fo[ob]ar</paragraph>' );
+			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
 
-			data.deleteContent( modelDocument.selection, modelDocument.batch() );
+			data.deleteContent( modelDocument.selection );
 
-			expect( getData( modelDocument ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
+		} );
+
+		it( 'should use parent batch', () => {
+			schema.registerItem( 'paragraph', '$block' );
+
+			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
+
+			model.change( writer => {
+				data.deleteContent( modelDocument.selection );
+				expect( writer.batch.deltas ).to.length( 1 );
+			} );
 		} );
 	} );
 
-	describe( 'modifySelection', () => {
+	describe( 'modifySelection()', () => {
 		it( 'should be decorated', () => {
 			schema.registerItem( 'paragraph', '$block' );
-			setData( modelDocument, '<paragraph>fo[ob]ar</paragraph>' );
+			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
 
 			const spy = sinon.spy();
 
@@ -418,22 +431,22 @@ describe( 'DataController', () => {
 		it( 'should modify a selection', () => {
 			schema.registerItem( 'paragraph', '$block' );
 
-			setData( modelDocument, '<paragraph>fo[ob]ar</paragraph>' );
+			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
 
 			data.modifySelection( modelDocument.selection, { direction: 'backward' } );
 
-			expect( getData( modelDocument ) ).to.equal( '<paragraph>fo[o]bar</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>fo[o]bar</paragraph>' );
 		} );
 	} );
 
-	describe( 'getSelectedContent', () => {
+	describe( 'getSelectedContent()', () => {
 		it( 'should be decorated', () => {
 			const spy = sinon.spy();
 			const sel = new ModelSelection();
 
 			data.on( 'getSelectedContent', spy );
 
-			data.getSelectedContent( sel, modelDocument.batch() );
+			data.getSelectedContent( sel );
 
 			expect( spy.calledOnce ).to.be.true;
 		} );
@@ -441,15 +454,26 @@ describe( 'DataController', () => {
 		it( 'should return selected content', () => {
 			schema.registerItem( 'paragraph', '$block' );
 
-			setData( modelDocument, '<paragraph>fo[ob]ar</paragraph>' );
+			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
 
-			const content = data.getSelectedContent( modelDocument.selection, modelDocument.batch() );
+			const content = data.getSelectedContent( modelDocument.selection );
 
 			expect( stringify( content ) ).to.equal( 'ob' );
 		} );
+
+		it( 'should use parent batch', () => {
+			schema.registerItem( 'paragraph', '$block' );
+
+			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
+
+			model.change( writer => {
+				data.getSelectedContent( modelDocument.selection );
+				expect( writer.batch.deltas ).to.length( 1 );
+			} );
+		} );
 	} );
 
-	describe( 'hasContent', () => {
+	describe( 'hasContent()', () => {
 		let root;
 
 		beforeEach( () => {
@@ -461,7 +485,7 @@ describe( 'DataController', () => {
 			schema.objects.add( 'image' );
 
 			setData(
-				modelDocument,
+				model,
 
 				'<div>' +
 					'<paragraph></paragraph>' +

+ 129 - 91
packages/ckeditor5-engine/tests/controller/deletecontent.js

@@ -3,23 +3,41 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../src/model/document';
+import Model from '../../src/model/model';
 import Position from '../../src/model/position';
 import Range from '../../src/model/range';
 import Element from '../../src/model/element';
+import DataController from '../../src/controller/datacontroller';
 import deleteContent from '../../src/controller/deletecontent';
 import { setData, getData } from '../../src/dev-utils/model';
 
-describe( 'DataController', () => {
-	let doc;
+describe( 'DataController utils', () => {
+	let model, doc, data;
 
 	describe( 'deleteContent', () => {
+		it( 'should use parent batch', () => {
+			model = new Model();
+			doc = model.document;
+			doc.createRoot();
+			data = new DataController( model );
+
+			model.schema.allow( { name: '$text', inside: '$root' } );
+			setData( model, 'x[abc]x' );
+
+			model.change( writer => {
+				deleteContent( data, doc.selection );
+				expect( writer.batch.deltas ).to.length( 1 );
+			} );
+		} );
+
 		describe( 'in simple scenarios', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'image', '$inline' );
 
@@ -40,11 +58,11 @@ describe( 'DataController', () => {
 			);
 
 			it( 'deletes single character (backward selection)', () => {
-				setData( doc, 'f[o]o', { lastRangeBackward: true } );
+				setData( model, 'f[o]o', { lastRangeBackward: true } );
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc ) ).to.equal( 'f[]o' );
+				expect( getData( model ) ).to.equal( 'f[]o' );
 			} );
 
 			test(
@@ -80,10 +98,12 @@ describe( 'DataController', () => {
 
 		describe( 'with text attributes', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'image', '$inline' );
 				schema.registerItem( 'paragraph', '$block' );
@@ -93,35 +113,35 @@ describe( 'DataController', () => {
 			} );
 
 			it( 'deletes characters (first half has attrs)', () => {
-				setData( doc, '<$text bold="true">fo[o</$text>b]ar' );
+				setData( model, '<$text bold="true">fo[o</$text>b]ar' );
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc ) ).to.equal( '<$text bold="true">fo[]</$text>ar' );
+				expect( getData( model ) ).to.equal( '<$text bold="true">fo[]</$text>ar' );
 				expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
 			} );
 
 			it( 'deletes characters (2nd half has attrs)', () => {
-				setData( doc, 'fo[o<$text bold="true">b]ar</$text>' );
+				setData( model, 'fo[o<$text bold="true">b]ar</$text>' );
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc ) ).to.equal( 'fo[]<$text bold="true">ar</$text>' );
+				expect( getData( model ) ).to.equal( 'fo[]<$text bold="true">ar</$text>' );
 				expect( doc.selection.getAttribute( 'bold' ) ).to.undefined;
 			} );
 
 			it( 'clears selection attrs when emptied content', () => {
-				setData( doc, '<paragraph>x</paragraph><paragraph>[<$text bold="true">foo</$text>]</paragraph><paragraph>y</paragraph>' );
+				setData( model, '<paragraph>x</paragraph><paragraph>[<$text bold="true">foo</$text>]</paragraph><paragraph>y</paragraph>' );
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc ) ).to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>y</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>y</paragraph>' );
 				expect( doc.selection.getAttribute( 'bold' ) ).to.undefined;
 			} );
 
 			it( 'leaves selection attributes when text contains them', () => {
 				setData(
-					doc,
+					model,
 					'<paragraph>x<$text bold="true">a[foo]b</$text>y</paragraph>',
 					{
 						selectionAttributes: {
@@ -130,9 +150,9 @@ describe( 'DataController', () => {
 					}
 				);
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc ) ).to.equal( '<paragraph>x<$text bold="true">a[]b</$text>y</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>x<$text bold="true">a[]b</$text>y</paragraph>' );
 				expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
 			} );
 		} );
@@ -148,10 +168,12 @@ describe( 'DataController', () => {
 		// Those case should, again, be handled by their specific implementations.
 		describe( 'in multi-element scenarios', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'paragraph', '$block' );
 				schema.registerItem( 'heading1', '$block' );
@@ -199,14 +221,14 @@ describe( 'DataController', () => {
 			// forward and backward delete.
 			it( 'merges second element into the first one (different name, backward selection)', () => {
 				setData(
-					doc,
+					model,
 					'<paragraph>x</paragraph><heading1>fo[o</heading1><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
 					{ lastRangeBackward: true }
 				);
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc ) ).to.equal( '<paragraph>x</paragraph><heading1>fo[]ar</heading1><paragraph>y</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>x</paragraph><heading1>fo[]ar</heading1><paragraph>y</paragraph>' );
 			} );
 
 			test(
@@ -234,44 +256,50 @@ describe( 'DataController', () => {
 			);
 
 			it( 'uses merge delta even if merged element is empty', () => {
-				setData( doc, '<paragraph>ab[cd</paragraph><paragraph>efgh]</paragraph>' );
+				let mergeSpy;
 
-				const batch = doc.batch();
-				const spyMerge = sinon.spy( batch, 'merge' );
+				setData( model, '<paragraph>ab[cd</paragraph><paragraph>efgh]</paragraph>' );
 
-				deleteContent( doc.selection, batch );
+				model.change( writer => {
+					mergeSpy = sinon.spy( writer, 'merge' );
+					deleteContent( data, doc.selection );
+				} );
 
-				expect( getData( doc ) ).to.equal( '<paragraph>ab[]</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>ab[]</paragraph>' );
 
-				expect( spyMerge.called ).to.be.true;
+				expect( mergeSpy.called ).to.be.true;
 			} );
 
 			it( 'uses merge delta even if merged element is empty #2', () => {
-				setData( doc, '<paragraph>ab[</paragraph><paragraph>]</paragraph>' );
+				let mergeSpy;
 
-				const batch = doc.batch();
-				const spyMerge = sinon.spy( batch, 'merge' );
+				setData( model, '<paragraph>ab[</paragraph><paragraph>]</paragraph>' );
 
-				deleteContent( doc.selection, batch );
+				model.change( writer => {
+					mergeSpy = sinon.spy( writer, 'merge' );
+					deleteContent( data, doc.selection );
+				} );
 
-				expect( getData( doc ) ).to.equal( '<paragraph>ab[]</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>ab[]</paragraph>' );
 
-				expect( spyMerge.called ).to.be.true;
+				expect( mergeSpy.called ).to.be.true;
 			} );
 
 			it( 'does not try to move the second block if not needed', () => {
-				setData( doc, '<paragraph>ab[cd</paragraph><paragraph>ef]gh</paragraph>' );
+				let mergeSpy, moveSpy;
 
-				const batch = doc.batch();
-				const spyMerge = sinon.spy( batch, 'merge' );
-				const spyMove = sinon.spy( batch, 'move' );
+				setData( model, '<paragraph>ab[cd</paragraph><paragraph>ef]gh</paragraph>' );
 
-				deleteContent( doc.selection, batch );
+				model.change( writer => {
+					mergeSpy = sinon.spy( writer, 'merge' );
+					moveSpy = sinon.spy( writer, 'move' );
+					deleteContent( data, doc.selection );
+				} );
 
-				expect( getData( doc ) ).to.equal( '<paragraph>ab[]gh</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>ab[]gh</paragraph>' );
 
-				expect( spyMove.called ).to.be.false;
-				expect( spyMerge.called ).to.be.true;
+				expect( moveSpy.called ).to.be.false;
+				expect( mergeSpy.called ).to.be.true;
 			} );
 
 			// Note: in all these cases we ignore the direction of merge.
@@ -318,9 +346,9 @@ describe( 'DataController', () => {
 
 					doc.selection.setRanges( [ range ] );
 
-					deleteContent( doc.selection, doc.batch() );
+					deleteContent( data, doc.selection );
 
-					expect( getData( doc ) )
+					expect( getData( model ) )
 						.to.equal( '<pparent>x<paragraph>x<pchild>fo[]ar</pchild>y</paragraph>y</pparent>' );
 				} );
 
@@ -363,9 +391,9 @@ describe( 'DataController', () => {
 
 					doc.selection.setRanges( [ range ] );
 
-					deleteContent( doc.selection, doc.batch() );
+					deleteContent( data, doc.selection );
 
-					expect( getData( doc ) )
+					expect( getData( model ) )
 						.to.equal( '<pparent>x<paragraph>foo<pchild>ba[]om</pchild></paragraph></pparent>' );
 				} );
 
@@ -406,16 +434,16 @@ describe( 'DataController', () => {
 
 					doc.selection.setRanges( [ range ] );
 
-					deleteContent( doc.selection, doc.batch() );
+					deleteContent( data, doc.selection );
 
-					expect( getData( doc ) )
+					expect( getData( model ) )
 						.to.equal( '<paragraph>fo[]</paragraph>' );
 				} );
 			} );
 
 			describe( 'with object elements', () => {
 				beforeEach( () => {
-					const schema = doc.schema;
+					const schema = model.schema;
 
 					schema.registerItem( 'blockWidget' );
 					schema.registerItem( 'nestedEditable' );
@@ -444,7 +472,7 @@ describe( 'DataController', () => {
 
 			describe( 'filtering out', () => {
 				beforeEach( () => {
-					const schema = doc.schema;
+					const schema = model.schema;
 
 					schema.allow( { name: '$text', attributes: [ 'a', 'b' ], inside: 'paragraph' } );
 					schema.allow( { name: '$text', attributes: [ 'b', 'c' ], inside: 'pchild' } );
@@ -490,7 +518,9 @@ describe( 'DataController', () => {
 
 		describe( 'in element selections scenarios', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
+
 				// <p> like root.
 				doc.createRoot( 'paragraph', 'paragraphRoot' );
 				// <body> like root.
@@ -498,7 +528,9 @@ describe( 'DataController', () => {
 				// Special root which allows only blockWidgets inside itself.
 				doc.createRoot( 'restrictedRoot', 'restrictedRoot' );
 
-				const schema = doc.schema;
+				data = new DataController( model );
+
+				const schema = model.schema;
 
 				schema.limits.add( 'restrictedRoot' );
 
@@ -517,105 +549,107 @@ describe( 'DataController', () => {
 			// See also "in simple scenarios => deletes an element".
 
 			it( 'deletes two inline elements', () => {
-				doc.schema.limits.add( 'paragraph' );
+				model.schema.limits.add( 'paragraph' );
 
 				setData(
-					doc,
+					model,
 					'x[<image></image><image></image>]z',
 					{ rootName: 'paragraphRoot' }
 				);
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc, { rootName: 'paragraphRoot' } ) )
+				expect( getData( model, { rootName: 'paragraphRoot' } ) )
 					.to.equal( 'x[]z' );
 			} );
 
 			it( 'creates a paragraph when text is not allowed (paragraph selected)', () => {
 				setData(
-					doc,
+					model,
 					'<paragraph>x</paragraph>[<paragraph>yyy</paragraph>]<paragraph>z</paragraph>',
 					{ rootName: 'bodyRoot' }
 				);
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc, { rootName: 'bodyRoot' } ) )
+				expect( getData( model, { rootName: 'bodyRoot' } ) )
 					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
 			} );
 
 			it( 'creates a paragraph when text is not allowed (block widget selected)', () => {
 				setData(
-					doc,
+					model,
 					'<paragraph>x</paragraph>[<blockWidget></blockWidget>]<paragraph>z</paragraph>',
 					{ rootName: 'bodyRoot' }
 				);
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc, { rootName: 'bodyRoot' } ) )
+				expect( getData( model, { rootName: 'bodyRoot' } ) )
 					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
 			} );
 
 			it( 'creates paragraph when text is not allowed (heading selected)', () => {
 				setData(
-					doc,
+					model,
 					'<paragraph>x</paragraph>[<heading1>yyy</heading1>]<paragraph>z</paragraph>',
 					{ rootName: 'bodyRoot' }
 				);
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc, { rootName: 'bodyRoot' } ) )
+				expect( getData( model, { rootName: 'bodyRoot' } ) )
 					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
 			} );
 
 			it( 'creates paragraph when text is not allowed (two blocks selected)', () => {
 				setData(
-					doc,
+					model,
 					'<paragraph>x</paragraph>[<heading1>yyy</heading1><paragraph>yyy</paragraph>]<paragraph>z</paragraph>',
 					{ rootName: 'bodyRoot' }
 				);
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc, { rootName: 'bodyRoot' } ) )
+				expect( getData( model, { rootName: 'bodyRoot' } ) )
 					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
 			} );
 
 			it( 'creates paragraph when text is not allowed (all content selected)', () => {
 				setData(
-					doc,
+					model,
 					'[<heading1>x</heading1><paragraph>z</paragraph>]',
 					{ rootName: 'bodyRoot' }
 				);
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc, { rootName: 'bodyRoot' } ) )
+				expect( getData( model, { rootName: 'bodyRoot' } ) )
 					.to.equal( '<paragraph>[]</paragraph>' );
 			} );
 
 			it( 'does not create a paragraph when it is not allowed', () => {
 				setData(
-					doc,
+					model,
 					'<blockWidget></blockWidget>[<blockWidget></blockWidget>]<blockWidget></blockWidget>',
 					{ rootName: 'restrictedRoot' }
 				);
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc, { rootName: 'restrictedRoot' } ) )
+				expect( getData( model, { rootName: 'restrictedRoot' } ) )
 					.to.equal( '<blockWidget></blockWidget>[]<blockWidget></blockWidget>' );
 			} );
 		} );
 
 		describe( 'integration with inline limit elements', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'inlineLimit' );
 				schema.allow( { name: 'inlineLimit', inside: '$root' } );
@@ -668,10 +702,12 @@ describe( 'DataController', () => {
 
 		describe( 'integration with block limit elements', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'blockLimit' );
 				schema.allow( { name: 'blockLimit', inside: '$root' } );
@@ -715,10 +751,12 @@ describe( 'DataController', () => {
 
 		describe( 'should leave a paragraph if the entire content was selected', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'div', '$block' );
 				schema.limits.add( 'div' );
@@ -789,14 +827,14 @@ describe( 'DataController', () => {
 				doc.createRoot( 'paragraph', 'paragraphRoot' );
 
 				setData(
-					doc,
+					model,
 					'x[<image></image><image></image>]z',
 					{ rootName: 'paragraphRoot' }
 				);
 
-				deleteContent( doc.selection, doc.batch() );
+				deleteContent( data, doc.selection );
 
-				expect( getData( doc, { rootName: 'paragraphRoot' } ) )
+				expect( getData( model, { rootName: 'paragraphRoot' } ) )
 					.to.equal( 'x[]z' );
 			} );
 
@@ -812,11 +850,11 @@ describe( 'DataController', () => {
 
 		function test( title, input, output, options ) {
 			it( title, () => {
-				setData( doc, input );
+				setData( model, input );
 
-				deleteContent( doc.selection, doc.batch(), options );
+				deleteContent( data, doc.selection, options );
 
-				expect( getData( doc ) ).to.equal( output );
+				expect( getData( model ) ).to.equal( output );
 			} );
 		}
 	} );

+ 46 - 44
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -15,7 +15,7 @@ import Mapper from '../../src/conversion/mapper';
 import ModelConversionDispatcher from '../../src/conversion/modelconversiondispatcher';
 import buildModelConverter from '../../src/conversion/buildmodelconverter';
 
-import ModelDocument from '../../src/model/document';
+import Model from '../../src/model/model';
 import ModelPosition from '../../src/model/position';
 import ModelElement from '../../src/model/element';
 import ModelText from '../../src/model/text';
@@ -32,7 +32,7 @@ describe( 'EditingController', () => {
 		let model, editing;
 
 		beforeEach( () => {
-			model = new ModelDocument();
+			model = new Model();
 			editing = new EditingController( model );
 		} );
 
@@ -63,9 +63,9 @@ describe( 'EditingController', () => {
 		let model, modelRoot, editing;
 
 		beforeEach( () => {
-			model = new ModelDocument();
-			modelRoot = model.createRoot();
-			model.createRoot( '$root', 'header' );
+			model = new Model();
+			modelRoot = model.document.createRoot();
+			model.document.createRoot( '$root', 'header' );
 
 			editing = new EditingController( model );
 		} );
@@ -101,8 +101,8 @@ describe( 'EditingController', () => {
 			expect( editing.view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domRoot );
 			expect( editing.view.renderer.markedChildren.has( viewRoot ) ).to.be.true;
 
-			expect( editing.mapper.toModelElement( viewRoot ) ).to.equal( model.getRoot( 'header' ) );
-			expect( editing.mapper.toViewElement( model.getRoot( 'header' ) ) ).to.equal( viewRoot );
+			expect( editing.mapper.toModelElement( viewRoot ) ).to.equal( model.document.getRoot( 'header' ) );
+			expect( editing.mapper.toViewElement( model.document.getRoot( 'header' ) ) ).to.equal( viewRoot );
 		} );
 
 		it( 'should be possible to attach DOM element later', () => {
@@ -131,12 +131,14 @@ describe( 'EditingController', () => {
 		beforeEach( () => {
 			listener = Object.create( EmitterMixin );
 
-			model = new ModelDocument();
-			modelRoot = model.createRoot();
+			model = new Model();
+			modelRoot = model.document.createRoot();
 
 			editing = new EditingController( model );
 
 			domRoot = document.createElement( 'div' );
+			domRoot.contentEditable = true;
+
 			document.body.appendChild( domRoot );
 			viewRoot = editing.createRoot( domRoot );
 
@@ -146,7 +148,7 @@ describe( 'EditingController', () => {
 			buildModelConverter().for( editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
 
 			// Note: The below code is highly overcomplicated due to #455.
-			model.selection.removeAllRanges();
+			model.document.selection.removeAllRanges();
 			modelRoot.removeChildren( 0, modelRoot.childCount );
 
 			viewRoot.removeChildren( 0, viewRoot.childCount );
@@ -155,13 +157,12 @@ describe( 'EditingController', () => {
 				'<paragraph>foo</paragraph>' +
 				'<paragraph></paragraph>' +
 				'<paragraph>bar</paragraph>',
-				model.schema,
-				model.batch()
+				model.schema
 			)._children );
 
-			model.enqueueChanges( () => {
-				model.batch().insert( modelData, model.getRoot() );
-				model.selection.addRange( ModelRange.createFromParentsAndOffsets(
+			model.enqueueChange( writer => {
+				writer.insert( modelData, model.document.getRoot() );
+				model.document.selection.addRange( ModelRange.createFromParentsAndOffsets(
 					modelRoot.getChild( 0 ), 1, modelRoot.getChild( 0 ), 1 ) );
 			} );
 		} );
@@ -179,9 +180,9 @@ describe( 'EditingController', () => {
 		it( 'should convert split', () => {
 			expect( getViewData( editing.view ) ).to.equal( '<p>f{}oo</p><p></p><p>bar</p>' );
 
-			model.enqueueChanges( () => {
-				model.batch().split( model.selection.getFirstPosition() );
-				model.selection.setRanges( [
+			model.enqueueChange( writer => {
+				writer.split( model.document.selection.getFirstPosition() );
+				model.document.selection.setRanges( [
 					ModelRange.createFromParentsAndOffsets(	modelRoot.getChild( 1 ), 0, modelRoot.getChild( 1 ), 0 )
 				] );
 			} );
@@ -192,19 +193,19 @@ describe( 'EditingController', () => {
 		it( 'should convert rename', () => {
 			expect( getViewData( editing.view ) ).to.equal( '<p>f{}oo</p><p></p><p>bar</p>' );
 
-			model.enqueueChanges( () => {
-				model.batch().rename( modelRoot.getChild( 0 ), 'div' );
+			model.enqueueChange( writer => {
+				writer.rename( modelRoot.getChild( 0 ), 'div' );
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<div>f{}oo</div><p></p><p>bar</p>' );
 		} );
 
 		it( 'should convert delete', () => {
-			model.enqueueChanges( () => {
-				model.batch().remove(
-					ModelRange.createFromPositionAndShift( model.selection.getFirstPosition(), 1 )
+			model.enqueueChange( writer => {
+				writer.remove(
+					ModelRange.createFromPositionAndShift( model.document.selection.getFirstPosition(), 1 )
 				);
-				model.selection.setRanges( [
+				model.document.selection.setRanges( [
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 1, modelRoot.getChild( 0 ), 1 )
 				] );
 			} );
@@ -218,12 +219,15 @@ describe( 'EditingController', () => {
 					expect( getModelData( model ) ).to.equal(
 						'<paragraph>foo</paragraph>' +
 						'<paragraph></paragraph>' +
-						'<paragraph>b[a]r</paragraph>' );
+						'<paragraph>b[a]r</paragraph>'
+					);
+
 					done();
 				} );
 			} );
 
 			editing.view.isFocused = true;
+			editing.view.render();
 
 			const domSelection = document.getSelection();
 			domSelection.removeAllRanges();
@@ -235,8 +239,8 @@ describe( 'EditingController', () => {
 		} );
 
 		it( 'should convert collapsed selection', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
+			model.enqueueChange( () => {
+				model.document.selection.setRanges( [
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 1 )
 				] );
 			} );
@@ -245,8 +249,8 @@ describe( 'EditingController', () => {
 		} );
 
 		it( 'should convert not collapsed selection', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
+			model.enqueueChange( () => {
+				model.document.selection.setRanges( [
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 2 )
 				] );
 			} );
@@ -255,16 +259,16 @@ describe( 'EditingController', () => {
 		} );
 
 		it( 'should clear previous selection', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
+			model.enqueueChange( () => {
+				model.document.selection.setRanges( [
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 1 )
 				] );
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>foo</p><p></p><p>b{}ar</p>' );
 
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
+			model.enqueueChange( () => {
+				model.document.selection.setRanges( [
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 2, modelRoot.getChild( 2 ), 2 )
 				] );
 			} );
@@ -375,8 +379,8 @@ describe( 'EditingController', () => {
 		} );
 
 		it( 'should forward add marker event if content is moved into a marker range', () => {
-			model.enqueueChanges( () => {
-				model.batch().appendElement( 'paragraph', model.getRoot() );
+			model.enqueueChange( writer => {
+				writer.appendElement( 'paragraph', model.document.getRoot() );
 			} );
 
 			const markerRange = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 );
@@ -398,8 +402,8 @@ describe( 'EditingController', () => {
 
 	describe( 'destroy()', () => {
 		it( 'should remove listenters', () => {
-			const model = new ModelDocument();
-			model.createRoot();
+			const model = new Model();
+			model.document.createRoot();
 			model.schema.registerItem( 'paragraph', '$block' );
 
 			const editing = new EditingController( model );
@@ -410,11 +414,9 @@ describe( 'EditingController', () => {
 
 			editing.destroy();
 
-			const batch = model.batch();
-
-			model.enqueueChanges( () => {
-				const modelData = parse( '<paragraph>foo</paragraph>', model.schema, batch ).getChild( 0 );
-				batch.insert( modelData, model.getRoot() );
+			model.enqueueChange( writer => {
+				const modelData = parse( '<paragraph>foo</paragraph>', model.schema ).getChild( 0 );
+				writer.insert( modelData, model.document.getRoot() );
 			} );
 
 			expect( spy.called ).to.be.false;
@@ -423,8 +425,8 @@ describe( 'EditingController', () => {
 		} );
 
 		it( 'should destroy view', () => {
-			const model = new ModelDocument();
-			model.createRoot();
+			const model = new Model();
+			model.document.createRoot();
 			model.schema.registerItem( 'paragraph', '$block' );
 
 			const editing = new EditingController( model );

+ 101 - 79
packages/ckeditor5-engine/tests/controller/getselectedcontent.js

@@ -3,21 +3,39 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../src/model/document';
+import Model from '../../src/model/model';
+import DataController from '../../src/controller/datacontroller';
 import DocumentFragment from '../../src/model/documentfragment';
 import getSelectedContent from '../../src/controller/getselectedcontent';
 import { setData, stringify } from '../../src/dev-utils/model';
 
-describe( 'Delete utils', () => {
-	let doc;
+describe( 'DataController utils', () => {
+	let model, doc, data;
 
 	describe( 'getSelectedContent', () => {
+		it( 'should use parent batch', () => {
+			model = new Model();
+			doc = model.document;
+			doc.createRoot();
+			data = new DataController( model );
+
+			model.schema.allow( { name: '$text', inside: '$root' } );
+			setData( model, 'x[abc]x' );
+
+			model.change( writer => {
+				getSelectedContent( data, doc.selection );
+				expect( writer.batch.deltas ).to.length( 1 );
+			} );
+		} );
+
 		describe( 'in simple scenarios', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'image', '$inline' );
 
@@ -28,27 +46,27 @@ describe( 'Delete utils', () => {
 			} );
 
 			it( 'returns empty fragment for no selection', () => {
-				setData( doc, 'abc' );
+				setData( model, 'abc' );
 
-				const frag = getSelectedContent( doc.selection, doc.batch() );
+				const frag = getSelectedContent( data, doc.selection );
 
 				expect( frag ).instanceOf( DocumentFragment );
 				expect( frag.isEmpty ).to.be.true;
 			} );
 
 			it( 'returns empty fragment for empty selection', () => {
-				setData( doc, 'a[]bc' );
+				setData( model, 'a[]bc' );
 
-				const frag = getSelectedContent( doc.selection, doc.batch() );
+				const frag = getSelectedContent( data, doc.selection );
 
 				expect( frag ).instanceOf( DocumentFragment );
 				expect( frag.isEmpty ).to.be.true;
 			} );
 
 			it( 'gets one character', () => {
-				setData( doc, 'a[b]c' );
+				setData( model, 'a[b]c' );
 
-				const frag = getSelectedContent( doc.selection, doc.batch() );
+				const frag = getSelectedContent( data, doc.selection );
 				const content = stringify( frag );
 
 				expect( frag ).instanceOf( DocumentFragment );
@@ -56,61 +74,63 @@ describe( 'Delete utils', () => {
 			} );
 
 			it( 'gets full text', () => {
-				setData( doc, '[abc]' );
+				setData( model, '[abc]' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( 'abc' );
 			} );
 
 			it( 'gets text with an attribute', () => {
-				setData( doc, 'xxx<$text bold="true">a[b]c</$text>' );
+				setData( model, 'xxx<$text bold="true">a[b]c</$text>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<$text bold="true">b</$text>' );
 			} );
 
 			it( 'gets text with attributes', () => {
-				setData( doc, 'x<$text bold="true">a[b</$text><$text italic="true">c]d</$text>x' );
+				setData( model, 'x<$text bold="true">a[b</$text><$text italic="true">c]d</$text>x' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<$text bold="true">b</$text><$text italic="true">c</$text>' );
 			} );
 
 			it( 'gets text with and without attribute', () => {
-				setData( doc, '<$text bold="true">a[b</$text>c]d' );
+				setData( model, '<$text bold="true">a[b</$text>c]d' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<$text bold="true">b</$text>c' );
 			} );
 
 			it( 'gets text and element', () => {
-				setData( doc, '[ab<image></image>c]' );
+				setData( model, '[ab<image></image>c]' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( 'ab<image></image>c' );
 			} );
 
 			it( 'gets one element', () => {
-				setData( doc, 'a[<image></image>]b' );
+				setData( model, 'a[<image></image>]b' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<image></image>' );
 			} );
 
 			it( 'gets multiple elements', () => {
-				setData( doc, '[<image></image><image></image>]' );
+				setData( model, '[<image></image><image></image>]' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<image></image><image></image>' );
 			} );
 		} );
 
 		describe( 'in blocks', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'paragraph', '$block' );
 				schema.registerItem( 'heading1', '$block' );
@@ -126,119 +146,119 @@ describe( 'Delete utils', () => {
 			} );
 
 			it( 'gets one character', () => {
-				setData( doc, '<paragraph>a[b]c</paragraph>' );
+				setData( model, '<paragraph>a[b]c</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( 'b' );
 			} );
 
 			it( 'gets entire paragraph content', () => {
-				setData( doc, '<paragraph>[a<image></image>b]</paragraph>' );
+				setData( model, '<paragraph>[a<image></image>b]</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( 'a<image></image>b' );
 			} );
 
 			it( 'gets two blocks - partial, partial', () => {
-				setData( doc, '<heading1>a[bc</heading1><paragraph>de]f</paragraph>' );
+				setData( model, '<heading1>a[bc</heading1><paragraph>de]f</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<heading1>bc</heading1><paragraph>de</paragraph>' );
 			} );
 
 			it( 'gets two blocks - full, partial', () => {
-				setData( doc, '<heading1>[abc</heading1><paragraph>de]f</paragraph>' );
+				setData( model, '<heading1>[abc</heading1><paragraph>de]f</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<heading1>abc</heading1><paragraph>de</paragraph>' );
 			} );
 
 			it( 'gets two blocks - full, partial 2', () => {
-				setData( doc, '<heading1>[abc</heading1><paragraph>de<image></image>]f</paragraph>' );
+				setData( model, '<heading1>[abc</heading1><paragraph>de<image></image>]f</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<heading1>abc</heading1><paragraph>de<image></image></paragraph>' );
 			} );
 
 			it( 'gets two blocks - full, partial 3', () => {
-				setData( doc, '<heading1>x</heading1><heading1>[abc</heading1><paragraph><image></image>de]f</paragraph>' );
+				setData( model, '<heading1>x</heading1><heading1>[abc</heading1><paragraph><image></image>de]f</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<heading1>abc</heading1><paragraph><image></image>de</paragraph>' );
 			} );
 
 			it( 'gets two blocks - full, partial 4', () => {
-				setData( doc, '<heading1>[abc</heading1><paragraph>de]f<image></image></paragraph>' );
+				setData( model, '<heading1>[abc</heading1><paragraph>de]f<image></image></paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<heading1>abc</heading1><paragraph>de</paragraph>' );
 			} );
 
 			it( 'gets two blocks - partial, full', () => {
-				setData( doc, '<heading1>a[bc</heading1><paragraph>def]</paragraph>' );
+				setData( model, '<heading1>a[bc</heading1><paragraph>def]</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<heading1>bc</heading1><paragraph>def</paragraph>' );
 			} );
 
 			it( 'gets two blocks - partial, full 2', () => {
-				setData( doc, '<heading1>a[<image></image>bc</heading1><paragraph>def]</paragraph>' );
+				setData( model, '<heading1>a[<image></image>bc</heading1><paragraph>def]</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<heading1><image></image>bc</heading1><paragraph>def</paragraph>' );
 			} );
 
 			// See https://github.com/ckeditor/ckeditor5-engine/issues/652#issuecomment-261358484
 			it( 'gets two blocks - empty, full', () => {
-				setData( doc, '<heading1>abc[</heading1><paragraph>def]</paragraph>' );
+				setData( model, '<heading1>abc[</heading1><paragraph>def]</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<paragraph>def</paragraph>' );
 			} );
 
 			// See https://github.com/ckeditor/ckeditor5-engine/issues/652#issuecomment-261358484
 			it( 'gets two blocks - partial, empty', () => {
-				setData( doc, '<heading1>a[bc</heading1><paragraph>]def</paragraph>' );
+				setData( model, '<heading1>a[bc</heading1><paragraph>]def</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<heading1>bc</heading1>' );
 			} );
 
 			it( 'gets three blocks', () => {
-				setData( doc, '<heading1>a[bc</heading1><paragraph>x</paragraph><paragraph>de]f</paragraph>' );
+				setData( model, '<heading1>a[bc</heading1><paragraph>x</paragraph><paragraph>de]f</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<heading1>bc</heading1><paragraph>x</paragraph><paragraph>de</paragraph>' );
 			} );
 
 			it( 'gets block image', () => {
-				setData( doc, '<paragraph>a</paragraph>[<blockImage><caption>Foo</caption></blockImage>]<paragraph>b</paragraph>' );
+				setData( model, '<paragraph>a</paragraph>[<blockImage><caption>Foo</caption></blockImage>]<paragraph>b</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<blockImage><caption>Foo</caption></blockImage>' );
 			} );
 
 			it( 'gets two blocks', () => {
-				setData( doc, '<paragraph>a</paragraph>[<blockImage></blockImage><blockImage></blockImage>]<paragraph>b</paragraph>' );
+				setData( model, '<paragraph>a</paragraph>[<blockImage></blockImage><blockImage></blockImage>]<paragraph>b</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<blockImage></blockImage><blockImage></blockImage>' );
 			} );
 
 			// Purely related to the current implementation.
 			it( 'gets content when multiple text items needs to be removed from the right excess', () => {
-				setData( doc, '<paragraph>a[b</paragraph><paragraph>c]d<$text bold="true">e</$text>f</paragraph>' );
+				setData( model, '<paragraph>a[b</paragraph><paragraph>c]d<$text bold="true">e</$text>f</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content )
 					.to.equal( '<paragraph>b</paragraph><paragraph>c</paragraph>' );
 			} );
 
 			// Purely related to the current implementation.
 			it( 'gets content when multiple text items needs to be removed from the left excess', () => {
-				setData( doc, '<paragraph>a<$text bold="true">b</$text>c[d</paragraph><paragraph>e]f</paragraph>' );
+				setData( model, '<paragraph>a<$text bold="true">b</$text>c[d</paragraph><paragraph>e]f</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content )
 					.to.equal( '<paragraph>d</paragraph><paragraph>e</paragraph>' );
 			} );
@@ -246,10 +266,12 @@ describe( 'Delete utils', () => {
 
 		describe( 'in blocks (deeply nested)', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'paragraph', '$block' );
 				schema.registerItem( 'heading1', '$block' );
@@ -260,62 +282,62 @@ describe( 'Delete utils', () => {
 			} );
 
 			it( 'gets content when ends are equally deeply nested', () => {
-				setData( doc, '<heading1>x</heading1><quote><paragraph>a[bc</paragraph><paragraph>de]f</paragraph></quote>' );
+				setData( model, '<heading1>x</heading1><quote><paragraph>a[bc</paragraph><paragraph>de]f</paragraph></quote>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<paragraph>bc</paragraph><paragraph>de</paragraph>' );
 			} );
 
 			it( 'gets content when left end nested deeper', () => {
-				setData( doc, '<quote><paragraph>a[bc</paragraph></quote><paragraph>de]f</paragraph>' );
+				setData( model, '<quote><paragraph>a[bc</paragraph></quote><paragraph>de]f</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<quote><paragraph>bc</paragraph></quote><paragraph>de</paragraph>' );
 			} );
 
 			it( 'gets content when left end nested deeper 2', () => {
-				setData( doc, '<quote><paragraph>a[bc</paragraph><heading1>x</heading1></quote><paragraph>de]f</paragraph>' );
+				setData( model, '<quote><paragraph>a[bc</paragraph><heading1>x</heading1></quote><paragraph>de]f</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<quote><paragraph>bc</paragraph><heading1>x</heading1></quote><paragraph>de</paragraph>' );
 			} );
 
 			it( 'gets content when left end nested deeper 3', () => {
-				setData( doc, '<quote><heading1>x</heading1><paragraph>a[bc</paragraph></quote><paragraph>de]f</paragraph>' );
+				setData( model, '<quote><heading1>x</heading1><paragraph>a[bc</paragraph></quote><paragraph>de]f</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<quote><paragraph>bc</paragraph></quote><paragraph>de</paragraph>' );
 			} );
 
 			// See https://github.com/ckeditor/ckeditor5-engine/issues/652#issuecomment-261358484
 			it( 'gets content when left end nested deeper 4', () => {
-				setData( doc, '<quote><heading1>x[</heading1><paragraph>abc</paragraph></quote><paragraph>de]f</paragraph>' );
+				setData( model, '<quote><heading1>x[</heading1><paragraph>abc</paragraph></quote><paragraph>de]f</paragraph>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<quote><paragraph>abc</paragraph></quote><paragraph>de</paragraph>' );
 			} );
 
 			it( 'gets content when right end nested deeper', () => {
-				setData( doc, '<paragraph>a[bc</paragraph><quote><paragraph>de]f</paragraph></quote>' );
+				setData( model, '<paragraph>a[bc</paragraph><quote><paragraph>de]f</paragraph></quote>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content ).to.equal( '<paragraph>bc</paragraph><quote><paragraph>de</paragraph></quote>' );
 			} );
 
 			it( 'gets content when both ends nested deeper than the middle element', () => {
-				setData( doc, '<quote><heading1>a[bc</heading1></quote><heading1>x</heading1><quote><heading1>de]f</heading1></quote>' );
+				setData( model, '<quote><heading1>a[bc</heading1></quote><heading1>x</heading1><quote><heading1>de]f</heading1></quote>' );
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content )
 					.to.equal( '<quote><heading1>bc</heading1></quote><heading1>x</heading1><quote><heading1>de</heading1></quote>' );
 			} );
 
 			// See: https://github.com/ckeditor/ckeditor5-engine/pull/1043#issuecomment-318012286
 			it( 'ensures that elements are retrieved by indexes instead of offsets', () => {
-				doc.schema.allow( { name: '$text', inside: '$root' } );
-				doc.schema.allow( { name: '$text', inside: 'quote' } );
+				model.schema.allow( { name: '$text', inside: '$root' } );
+				model.schema.allow( { name: '$text', inside: 'quote' } );
 
-				setData( doc,
+				setData( model,
 					'foo' +
 					'<quote>' +
 						'<paragraph>' +
@@ -325,7 +347,7 @@ describe( 'Delete utils', () => {
 					'</quote>'
 				);
 
-				const content = stringify( getSelectedContent( doc.selection, doc.batch() ) );
+				const content = stringify( getSelectedContent( data, doc.selection ) );
 				expect( content )
 					.to.equal( '<paragraph>ar</paragraph>bo' );
 			} );

+ 217 - 217
packages/ckeditor5-engine/tests/controller/insertcontent.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../src/model/document';
+import Model from '../../src/model/model';
 import DataController from '../../src/controller/datacontroller';
 import insertContent from '../../src/controller/insertcontent';
 
@@ -13,83 +13,82 @@ import Element from '../../src/model/element';
 
 import { setData, getData, parse } from '../../src/dev-utils/model';
 
-describe( 'DataController', () => {
-	let doc, dataController;
+describe( 'DataController utils', () => {
+	let model, doc, data;
 
 	describe( 'insertContent', () => {
-		it( 'uses the passed batch', () => {
-			const doc = new Document();
+		it( 'should use parent batch', () => {
+			model = new Model();
+			doc = model.document;
 			doc.createRoot();
-			doc.schema.allow( { name: '$text', inside: '$root' } );
+			data = new DataController( model );
 
-			const dataController = new DataController( doc );
+			model.schema.allow( { name: '$text', inside: '$root' } );
+			setData( model, 'x[]x' );
 
-			const batch = doc.batch();
-
-			setData( doc, 'x[]x' );
-
-			insertContent( dataController, new DocumentFragment( [ new Text( 'a' ) ] ), doc.selection, batch );
-
-			expect( batch.deltas.length ).to.be.above( 0 );
+			model.change( writer => {
+				insertContent( data, new Text( 'a' ), doc.selection );
+				expect( writer.batch.deltas ).to.length( 1 );
+			} );
 		} );
 
 		it( 'accepts DocumentFragment', () => {
-			const doc = new Document();
-			const dataController = new DataController( doc );
-			const batch = doc.batch();
-
+			model = new Model();
+			doc = model.document;
 			doc.createRoot();
-			doc.schema.allow( { name: '$text', inside: '$root' } );
+			data = new DataController( model );
+
+			model.schema.allow( { name: '$text', inside: '$root' } );
 
-			setData( doc, 'x[]x' );
+			setData( model, 'x[]x' );
 
-			insertContent( dataController, new DocumentFragment( [ new Text( 'a' ) ] ), doc.selection, batch );
+			insertContent( data, new DocumentFragment( [ new Text( 'a' ) ] ), doc.selection );
 
-			expect( getData( doc ) ).to.equal( 'xa[]x' );
+			expect( getData( model ) ).to.equal( 'xa[]x' );
 		} );
 
 		it( 'accepts Text', () => {
-			const doc = new Document();
-			const dataController = new DataController( doc );
-			const batch = doc.batch();
-
+			model = new Model();
+			doc = model.document;
 			doc.createRoot();
-			doc.schema.allow( { name: '$text', inside: '$root' } );
+			data = new DataController( model );
+
+			model.schema.allow( { name: '$text', inside: '$root' } );
 
-			setData( doc, 'x[]x' );
+			setData( model, 'x[]x' );
 
-			insertContent( dataController, new Text( 'a' ), doc.selection, batch );
+			insertContent( data, new Text( 'a' ), doc.selection );
 
-			expect( getData( doc ) ).to.equal( 'xa[]x' );
+			expect( getData( model ) ).to.equal( 'xa[]x' );
 		} );
 
 		it( 'should save the reference to the original object', () => {
-			const doc = new Document();
-			const dataController = new DataController( doc );
-			const batch = doc.batch();
-			const content = new Element( 'image' );
-
+			model = new Model();
+			doc = model.document;
 			doc.createRoot();
+			data = new DataController( model );
+
+			const content = new Element( 'image' );
 
-			doc.schema.registerItem( 'paragraph', '$block' );
-			doc.schema.registerItem( 'image', '$inline' );
-			doc.schema.objects.add( 'image' );
+			model.schema.registerItem( 'paragraph', '$block' );
+			model.schema.registerItem( 'image', '$inline' );
+			model.schema.objects.add( 'image' );
 
-			setData( doc, '<paragraph>foo[]</paragraph>' );
+			setData( model, '<paragraph>foo[]</paragraph>' );
 
-			insertContent( dataController, content, doc.selection, batch );
+			insertContent( data, content, doc.selection );
 
 			expect( doc.getRoot().getChild( 0 ).getChild( 1 ) ).to.equal( content );
 		} );
 
 		describe( 'in simple scenarios', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				dataController = new DataController( doc );
-
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'image', '$inline' );
 				schema.registerItem( 'disallowedElement' );
@@ -98,7 +97,7 @@ describe( 'DataController', () => {
 				schema.allow( { name: 'image', inside: '$root' } );
 				// Otherwise it won't be passed to the temporary model fragment used inside insert().
 				schema.allow( { name: 'disallowedElement', inside: '$clipboardHolder' } );
-				doc.schema.allow( { name: '$text', inside: 'disallowedElement' } );
+				model.schema.allow( { name: '$text', inside: 'disallowedElement' } );
 
 				schema.allow( { name: '$inline', attributes: [ 'bold' ] } );
 				schema.allow( { name: '$inline', attributes: [ 'italic' ] } );
@@ -107,29 +106,29 @@ describe( 'DataController', () => {
 			} );
 
 			it( 'inserts one text node', () => {
-				setData( doc, 'f[]oo' );
+				setData( model, 'f[]oo' );
 				insertHelper( 'xyz' );
-				expect( getData( doc ) ).to.equal( 'fxyz[]oo' );
+				expect( getData( model ) ).to.equal( 'fxyz[]oo' );
 			} );
 
 			it( 'inserts one text node (at the end)', () => {
-				setData( doc, 'foo[]' );
+				setData( model, 'foo[]' );
 				insertHelper( 'xyz' );
-				expect( getData( doc ) ).to.equal( 'fooxyz[]' );
+				expect( getData( model ) ).to.equal( 'fooxyz[]' );
 			} );
 
 			it( 'inserts one text node with attribute', () => {
-				setData( doc, 'f[]oo' );
+				setData( model, 'f[]oo' );
 				insertHelper( '<$text bold="true">xyz</$text>' );
-				expect( getData( doc ) ).to.equal( 'f<$text bold="true">xyz[]</$text>oo' );
+				expect( getData( model ) ).to.equal( 'f<$text bold="true">xyz[]</$text>oo' );
 
 				expect( doc.selection.getAttribute( 'bold' ) ).to.be.true;
 			} );
 
 			it( 'inserts one text node with attribute into text with a different attribute', () => {
-				setData( doc, '<$text bold="true">f[]oo</$text>' );
+				setData( model, '<$text bold="true">f[]oo</$text>' );
 				insertHelper( '<$text italic="true">xyz</$text>' );
-				expect( getData( doc ) )
+				expect( getData( model ) )
 					.to.equal( '<$text bold="true">f</$text><$text italic="true">xyz[]</$text><$text bold="true">oo</$text>' );
 
 				expect( doc.selection.getAttribute( 'italic' ) ).to.be.true;
@@ -137,44 +136,44 @@ describe( 'DataController', () => {
 			} );
 
 			it( 'inserts one text node with attribute into text with the same attribute', () => {
-				setData( doc, '<$text bold="true">f[]oo</$text>' );
+				setData( model, '<$text bold="true">f[]oo</$text>' );
 				insertHelper( '<$text bold="true">xyz</$text>' );
-				expect( getData( doc ) )
+				expect( getData( model ) )
 					.to.equal( '<$text bold="true">fxyz[]oo</$text>' );
 
 				expect( doc.selection.getAttribute( 'bold' ) ).to.be.true;
 			} );
 
 			it( 'inserts a text without attributes into a text with an attribute', () => {
-				setData( doc, '<$text bold="true">f[]oo</$text>' );
+				setData( model, '<$text bold="true">f[]oo</$text>' );
 				insertHelper( 'xyz' );
-				expect( getData( doc ) ).to.equal( '<$text bold="true">f</$text>xyz[]<$text bold="true">oo</$text>' );
+				expect( getData( model ) ).to.equal( '<$text bold="true">f</$text>xyz[]<$text bold="true">oo</$text>' );
 
 				expect( doc.selection.hasAttribute( 'bold' ) ).to.be.false;
 			} );
 
 			it( 'inserts an element', () => {
-				setData( doc, 'f[]oo' );
+				setData( model, 'f[]oo' );
 				insertHelper( '<image></image>' );
-				expect( getData( doc ) ).to.equal( 'f<image></image>[]oo' );
+				expect( getData( model ) ).to.equal( 'f<image></image>[]oo' );
 			} );
 
 			it( 'inserts a text and an element', () => {
-				setData( doc, 'f[]oo' );
+				setData( model, 'f[]oo' );
 				insertHelper( 'xyz<image></image>' );
-				expect( getData( doc ) ).to.equal( 'fxyz<image></image>[]oo' );
+				expect( getData( model ) ).to.equal( 'fxyz<image></image>[]oo' );
 			} );
 
 			it( 'strips a disallowed element', () => {
-				setData( doc, 'f[]oo' );
+				setData( model, 'f[]oo' );
 				insertHelper( '<disallowedElement>xyz</disallowedElement>' );
-				expect( getData( doc ) ).to.equal( 'fxyz[]oo' );
+				expect( getData( model ) ).to.equal( 'fxyz[]oo' );
 			} );
 
 			it( 'deletes selection before inserting the content', () => {
-				setData( doc, 'f[abc]oo' );
+				setData( model, 'f[abc]oo' );
 				insertHelper( 'x' );
-				expect( getData( doc ) ).to.equal( 'fx[]oo' );
+				expect( getData( model ) ).to.equal( 'fx[]oo' );
 			} );
 
 			describe( 'spaces handling', () => {
@@ -182,45 +181,45 @@ describe( 'DataController', () => {
 				// inserted into the model as is. The conversion to nbsps happen on view<=>DOM conversion.
 
 				it( 'inserts one space', () => {
-					setData( doc, 'f[]oo' );
+					setData( model, 'f[]oo' );
 					insertHelper( new Text( ' ' ) );
-					expect( getData( doc ) ).to.equal( 'f []oo' );
+					expect( getData( model ) ).to.equal( 'f []oo' );
 				} );
 
 				it( 'inserts three spaces', () => {
-					setData( doc, 'f[]oo' );
+					setData( model, 'f[]oo' );
 					insertHelper( new Text( '   ' ) );
-					expect( getData( doc ) ).to.equal( 'f   []oo' );
+					expect( getData( model ) ).to.equal( 'f   []oo' );
 				} );
 
 				it( 'inserts spaces at the end', () => {
-					setData( doc, 'foo[]' );
+					setData( model, 'foo[]' );
 					insertHelper( new Text( '   ' ) );
-					expect( getData( doc ) ).to.equal( 'foo   []' );
+					expect( getData( model ) ).to.equal( 'foo   []' );
 				} );
 
 				it( 'inserts one nbsp', () => {
-					setData( doc, 'f[]oo' );
+					setData( model, 'f[]oo' );
 					insertHelper( new Text( '\u200a' ) );
-					expect( getData( doc ) ).to.equal( 'f\u200a[]oo' );
+					expect( getData( model ) ).to.equal( 'f\u200a[]oo' );
 				} );
 
 				it( 'inserts word surrounded by spaces', () => {
-					setData( doc, 'f[]oo' );
+					setData( model, 'f[]oo' );
 					insertHelper( new Text( ' xyz  ' ) );
-					expect( getData( doc ) ).to.equal( 'f xyz  []oo' );
+					expect( getData( model ) ).to.equal( 'f xyz  []oo' );
 				} );
 			} );
 		} );
 
 		describe( 'in blocks', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				dataController = new DataController( doc );
-
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'paragraph', '$block' );
 				schema.registerItem( 'heading1', '$block' );
@@ -244,137 +243,137 @@ describe( 'DataController', () => {
 			} );
 
 			it( 'inserts one text node', () => {
-				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				setData( model, '<paragraph>f[]oo</paragraph>' );
 				insertHelper( 'xyz' );
-				expect( getData( doc ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
 			} );
 
 			it( 'inserts one text node to fully selected paragraph', () => {
-				setData( doc, '<paragraph>[foo]</paragraph>' );
+				setData( model, '<paragraph>[foo]</paragraph>' );
 				insertHelper( 'xyz' );
-				expect( getData( doc ) ).to.equal( '<paragraph>xyz[]</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>xyz[]</paragraph>' );
 			} );
 
 			it( 'inserts one text node to fully selected paragraphs (from outside)', () => {
-				setData( doc, '[<paragraph>foo</paragraph><paragraph>bar</paragraph>]' );
+				setData( model, '[<paragraph>foo</paragraph><paragraph>bar</paragraph>]' );
 				insertHelper( 'xyz' );
-				expect( getData( doc ) ).to.equal( '<paragraph>xyz[]</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>xyz[]</paragraph>' );
 			} );
 
 			it( 'merges two blocks before inserting content (p+p)', () => {
-				setData( doc, '<paragraph>fo[o</paragraph><paragraph>b]ar</paragraph>' );
+				setData( model, '<paragraph>fo[o</paragraph><paragraph>b]ar</paragraph>' );
 				insertHelper( 'xyz' );
-				expect( getData( doc ) ).to.equal( '<paragraph>foxyz[]ar</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>foxyz[]ar</paragraph>' );
 			} );
 
 			it( 'inserts inline widget and text', () => {
-				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				setData( model, '<paragraph>f[]oo</paragraph>' );
 				insertHelper( 'xyz<inlineWidget></inlineWidget>' );
-				expect( getData( doc ) ).to.equal( '<paragraph>fxyz<inlineWidget></inlineWidget>[]oo</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>fxyz<inlineWidget></inlineWidget>[]oo</paragraph>' );
 			} );
 
 			// Note: In CKEditor 4 the blocks are not merged, but to KISS we're merging here
 			// because that's what deleteContent() does.
 			it( 'merges two blocks before inserting content (h+p)', () => {
-				setData( doc, '<heading1>fo[o</heading1><paragraph>b]ar</paragraph>' );
+				setData( model, '<heading1>fo[o</heading1><paragraph>b]ar</paragraph>' );
 				insertHelper( 'xyz' );
-				expect( getData( doc ) ).to.equal( '<heading1>foxyz[]ar</heading1>' );
+				expect( getData( model ) ).to.equal( '<heading1>foxyz[]ar</heading1>' );
 			} );
 
 			it( 'not insert autoparagraph when paragraph is disallowed at the current position', () => {
-				doc.schema.disallow( { name: 'paragraph', inside: '$root' } );
+				model.schema.disallow( { name: 'paragraph', inside: '$root' } );
 
 				const content = new DocumentFragment( [
 					new Element( 'heading1', [], [ new Text( 'bar' ) ] ),
 					new Text( 'biz' )
 				] );
 
-				setData( doc, '[<heading2>foo</heading2>]' );
-				insertContent( dataController, content, doc.selection );
-				expect( getData( doc ) ).to.equal( '<heading1>bar[]</heading1>' );
+				setData( model, '[<heading2>foo</heading2>]' );
+				insertContent( data, content, doc.selection );
+				expect( getData( model ) ).to.equal( '<heading1>bar[]</heading1>' );
 			} );
 
 			describe( 'block to block handling', () => {
 				it( 'inserts one paragraph', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( '<paragraph>xyz</paragraph>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
 				} );
 
 				it( 'inserts one paragraph (at the end)', () => {
-					setData( doc, '<paragraph>foo[]</paragraph>' );
+					setData( model, '<paragraph>foo[]</paragraph>' );
 					insertHelper( '<paragraph>xyz</paragraph>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fooxyz[]</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fooxyz[]</paragraph>' );
 				} );
 
 				it( 'inserts one paragraph into an empty paragraph', () => {
-					setData( doc, '<paragraph>[]</paragraph>' );
+					setData( model, '<paragraph>[]</paragraph>' );
 					insertHelper( '<paragraph>xyz</paragraph>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>xyz[]</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>xyz[]</paragraph>' );
 				} );
 
 				it( 'inserts one empty paragraph', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( '<paragraph></paragraph>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>f[]oo</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>f[]oo</paragraph>' );
 				} );
 
 				it( 'inserts one block into a fully selected content', () => {
-					setData( doc, '<heading1>[foo</heading1><paragraph>bar]</paragraph>' );
+					setData( model, '<heading1>[foo</heading1><paragraph>bar]</paragraph>' );
 					insertHelper( '<heading2>xyz</heading2>' );
-					expect( getData( doc ) ).to.equal( '<heading2>xyz[]</heading2>' );
+					expect( getData( model ) ).to.equal( '<heading2>xyz[]</heading2>' );
 				} );
 
 				it( 'inserts one heading', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( '<heading1>xyz</heading1>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
 				} );
 
 				it( 'inserts two headings', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( '<heading1>xxx</heading1><heading1>yyy</heading1>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fxxx</paragraph><heading1>yyy[]oo</heading1>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fxxx</paragraph><heading1>yyy[]oo</heading1>' );
 				} );
 
 				it( 'inserts one object', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( '<blockWidget></blockWidget>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>f</paragraph>[<blockWidget></blockWidget>]<paragraph>oo</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>f</paragraph>[<blockWidget></blockWidget>]<paragraph>oo</paragraph>' );
 				} );
 
 				it( 'inserts one object (at the end)', () => {
-					setData( doc, '<paragraph>foo[]</paragraph>' );
+					setData( model, '<paragraph>foo[]</paragraph>' );
 					insertHelper( '<blockWidget></blockWidget>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
+					expect( getData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
 				} );
 
 				it( 'inserts one object (at the beginning)', () => {
-					setData( doc, '<paragraph>[]bar</paragraph>' );
+					setData( model, '<paragraph>[]bar</paragraph>' );
 					insertHelper( '<blockWidget></blockWidget>' );
-					expect( getData( doc ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					expect( getData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
 				} );
 
 				it( 'inserts one list item', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( '<listItem indent="0" type="bulleted">xyz</listItem>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
 				} );
 
 				it( 'inserts list item to empty element', () => {
-					setData( doc, '<paragraph>[]</paragraph>' );
+					setData( model, '<paragraph>[]</paragraph>' );
 					insertHelper( '<listItem indent="0" type="bulleted">xyz</listItem>' );
-					expect( getData( doc ) ).to.equal( '<listItem indent="0" type="bulleted">xyz[]</listItem>' );
+					expect( getData( model ) ).to.equal( '<listItem indent="0" type="bulleted">xyz[]</listItem>' );
 				} );
 
 				it( 'inserts three list items at the end of paragraph', () => {
-					setData( doc, '<paragraph>foo[]</paragraph>' );
+					setData( model, '<paragraph>foo[]</paragraph>' );
 					insertHelper(
 						'<listItem indent="0" type="bulleted">xxx</listItem>' +
 						'<listItem indent="0" type="bulleted">yyy</listItem>' +
 						'<listItem indent="0" type="bulleted">zzz</listItem>'
 					);
-					expect( getData( doc ) ).to.equal(
+					expect( getData( model ) ).to.equal(
 						'<paragraph>fooxxx</paragraph>' +
 						'<listItem indent="0" type="bulleted">yyy</listItem>' +
 						'<listItem indent="0" type="bulleted">zzz[]</listItem>'
@@ -382,12 +381,12 @@ describe( 'DataController', () => {
 				} );
 
 				it( 'inserts two list items to an empty paragraph', () => {
-					setData( doc, '<paragraph>a</paragraph><paragraph>[]</paragraph><paragraph>b</paragraph>' );
+					setData( model, '<paragraph>a</paragraph><paragraph>[]</paragraph><paragraph>b</paragraph>' );
 					insertHelper(
 						'<listItem indent="0" type="bulleted">xxx</listItem>' +
 						'<listItem indent="0" type="bulleted">yyy</listItem>'
 					);
-					expect( getData( doc ) ).to.equal(
+					expect( getData( model ) ).to.equal(
 						'<paragraph>a</paragraph>' +
 						'<listItem indent="0" type="bulleted">xxx</listItem>' +
 						'<listItem indent="0" type="bulleted">yyy[]</listItem>' +
@@ -398,51 +397,51 @@ describe( 'DataController', () => {
 
 			describe( 'mixed content to block', () => {
 				it( 'inserts text + paragraph', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( 'xxx<paragraph>yyy</paragraph>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fxxx</paragraph><paragraph>yyy[]oo</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fxxx</paragraph><paragraph>yyy[]oo</paragraph>' );
 				} );
 
 				it( 'inserts text + inlineWidget + text + paragraph', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( 'xxx<inlineWidget></inlineWidget>yyy<paragraph>zzz</paragraph>' );
-					expect( getData( doc ) ).to.equal(
+					expect( getData( model ) ).to.equal(
 						'<paragraph>fxxx<inlineWidget></inlineWidget>yyy</paragraph><paragraph>zzz[]oo</paragraph>'
 					);
 				} );
 
 				it( 'inserts text + paragraph (at the beginning)', () => {
-					setData( doc, '<paragraph>[]foo</paragraph>' );
+					setData( model, '<paragraph>[]foo</paragraph>' );
 					insertHelper( 'xxx<paragraph>yyy</paragraph>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>xxx</paragraph><paragraph>yyy[]foo</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>xxx</paragraph><paragraph>yyy[]foo</paragraph>' );
 				} );
 
 				it( 'inserts text + paragraph (at the end)', () => {
-					setData( doc, '<paragraph>foo[]</paragraph>' );
+					setData( model, '<paragraph>foo[]</paragraph>' );
 					insertHelper( 'xxx<paragraph>yyy</paragraph>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fooxxx</paragraph><paragraph>yyy[]</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fooxxx</paragraph><paragraph>yyy[]</paragraph>' );
 				} );
 
 				it( 'inserts paragraph + text', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( '<paragraph>yyy</paragraph>xxx' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fyyy</paragraph><paragraph>xxx[]oo</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fyyy</paragraph><paragraph>xxx[]oo</paragraph>' );
 				} );
 
 				// This is the expected result, but it was so hard to achieve at this stage that I
 				// decided to go with the what the next test represents.
 				// it( 'inserts paragraph + text + inlineWidget + text', () => {
-				// 	setData( doc, '<paragraph>f[]oo</paragraph>' );
+				// 	setData( model, '<paragraph>f[]oo</paragraph>' );
 				// 	insertHelper( '<paragraph>yyy</paragraph>xxx<inlineWidget></inlineWidget>zzz' );
-				// 	expect( getData( doc ) )
+				// 	expect( getData( model ) )
 				// 		.to.equal( '<paragraph>fyyy</paragraph><paragraph>xxx<inlineWidget></inlineWidget>zzz[]oo</paragraph>' );
 				// } );
 
 				// See the comment above.
 				it( 'inserts paragraph + text + inlineWidget + text', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( '<paragraph>yyy</paragraph>xxx<inlineWidget></inlineWidget>zzz' );
-					expect( getData( doc ) ).to.equal(
+					expect( getData( model ) ).to.equal(
 						'<paragraph>fyyy</paragraph><paragraph>xxx</paragraph>' +
 						'<paragraph><inlineWidget></inlineWidget></paragraph>' +
 						'<paragraph>zzz[]oo</paragraph>'
@@ -450,43 +449,43 @@ describe( 'DataController', () => {
 				} );
 
 				it( 'inserts paragraph + text + paragraph', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( '<paragraph>yyy</paragraph>xxx<paragraph>zzz</paragraph>' );
-					expect( getData( doc ) ).to.equal(
+					expect( getData( model ) ).to.equal(
 						'<paragraph>fyyy</paragraph><paragraph>xxx</paragraph><paragraph>zzz[]oo</paragraph>'
 					);
 				} );
 
 				it( 'inserts paragraph + text (at the beginning)', () => {
-					setData( doc, '<paragraph>[]foo</paragraph>' );
+					setData( model, '<paragraph>[]foo</paragraph>' );
 					insertHelper( '<paragraph>yyy</paragraph>xxx' );
-					expect( getData( doc ) ).to.equal( '<paragraph>yyy</paragraph><paragraph>xxx[]foo</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>yyy</paragraph><paragraph>xxx[]foo</paragraph>' );
 				} );
 
 				it( 'inserts paragraph + text (at the end)', () => {
-					setData( doc, '<paragraph>foo[]</paragraph>' );
+					setData( model, '<paragraph>foo[]</paragraph>' );
 					insertHelper( '<paragraph>yyy</paragraph>xxx' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fooyyy</paragraph><paragraph>xxx[]</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fooyyy</paragraph><paragraph>xxx[]</paragraph>' );
 				} );
 
 				it( 'inserts text + heading', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( 'xxx<heading1>yyy</heading1>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fxxx</paragraph><heading1>yyy[]oo</heading1>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fxxx</paragraph><heading1>yyy[]oo</heading1>' );
 				} );
 
 				it( 'inserts paragraph + object', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( '<paragraph>xxx</paragraph><blockWidget></blockWidget>' );
-					expect( getData( doc ) ).to.equal(
+					expect( getData( model ) ).to.equal(
 						'<paragraph>fxxx</paragraph>[<blockWidget></blockWidget>]<paragraph>oo</paragraph>'
 					);
 				} );
 
 				it( 'inserts object + paragraph', () => {
-					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					setData( model, '<paragraph>f[]oo</paragraph>' );
 					insertHelper( '<blockWidget></blockWidget><paragraph>xxx</paragraph>' );
-					expect( getData( doc ) ).to.equal(
+					expect( getData( model ) ).to.equal(
 						'<paragraph>f</paragraph><blockWidget></blockWidget><paragraph>xxx[]oo</paragraph>'
 					);
 				} );
@@ -494,50 +493,51 @@ describe( 'DataController', () => {
 
 			describe( 'content over a block object', () => {
 				it( 'inserts text', () => {
-					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					setData( model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
 					insertHelper( 'xxx' );
-					expect( getData( doc ) ).to.equal(
+					expect( getData( model ) ).to.equal(
 						'<paragraph>foo</paragraph><paragraph>xxx[]</paragraph><paragraph>bar</paragraph>'
 					);
 				} );
 
 				it( 'inserts paragraph', () => {
-					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					setData( model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
 					insertHelper( '<paragraph>xxx</paragraph>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>foo</paragraph><paragraph>xxx[]</paragraph><paragraph>bar</paragraph>' );
+					expect( getData( model ) )
+						.to.equal( '<paragraph>foo</paragraph><paragraph>xxx[]</paragraph><paragraph>bar</paragraph>' );
 				} );
 
 				it( 'inserts text + paragraph', () => {
-					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					setData( model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
 					insertHelper( 'yyy<paragraph>xxx</paragraph>' );
-					expect( getData( doc ) )
+					expect( getData( model ) )
 						.to.equal(
 							'<paragraph>foo</paragraph><paragraph>yyy</paragraph><paragraph>xxx[]</paragraph><paragraph>bar</paragraph>'
 						);
 				} );
 
 				it( 'inserts two blocks', () => {
-					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					setData( model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
 					insertHelper( '<heading1>xxx</heading1><paragraph>yyy</paragraph>' );
-					expect( getData( doc ) )
+					expect( getData( model ) )
 						.to.equal(
 							'<paragraph>foo</paragraph><heading1>xxx</heading1><paragraph>yyy[]</paragraph><paragraph>bar</paragraph>'
 						);
 				} );
 
 				it( 'inserts block object', () => {
-					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					setData( model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
 					insertHelper( '<blockWidget></blockWidget>' );
 					// It's enough, don't worry.
-					expect( getData( doc ) ).to.equal(
+					expect( getData( model ) ).to.equal(
 						'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>'
 					);
 				} );
 
 				it( 'inserts inline object', () => {
-					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					setData( model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
 					insertHelper( '<inlineWidget></inlineWidget>' );
-					expect( getData( doc ) )
+					expect( getData( model ) )
 						.to.equal(
 							'<paragraph>foo</paragraph><paragraph><inlineWidget></inlineWidget>[]</paragraph><paragraph>bar</paragraph>'
 						);
@@ -546,39 +546,39 @@ describe( 'DataController', () => {
 
 			describe( 'content over an inline object', () => {
 				it( 'inserts text', () => {
-					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					setData( model, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
 					insertHelper( 'xxx' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fooxxx[]bar</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fooxxx[]bar</paragraph>' );
 				} );
 
 				it( 'inserts paragraph', () => {
-					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					setData( model, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
 					insertHelper( '<paragraph>xxx</paragraph>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fooxxx[]bar</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fooxxx[]bar</paragraph>' );
 				} );
 
 				it( 'inserts text + paragraph', () => {
-					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					setData( model, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
 					insertHelper( 'yyy<paragraph>xxx</paragraph>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fooyyy</paragraph><paragraph>xxx[]bar</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fooyyy</paragraph><paragraph>xxx[]bar</paragraph>' );
 				} );
 
 				it( 'inserts two blocks', () => {
-					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					setData( model, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
 					insertHelper( '<heading1>xxx</heading1><paragraph>yyy</paragraph>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>fooxxx</paragraph><paragraph>yyy[]bar</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>fooxxx</paragraph><paragraph>yyy[]bar</paragraph>' );
 				} );
 
 				it( 'inserts inline object', () => {
-					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					setData( model, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
 					insertHelper( '<inlineWidget></inlineWidget>' );
-					expect( getData( doc ) ).to.equal( '<paragraph>foo<inlineWidget></inlineWidget>[]bar</paragraph>' );
+					expect( getData( model ) ).to.equal( '<paragraph>foo<inlineWidget></inlineWidget>[]bar</paragraph>' );
 				} );
 
 				it( 'inserts block object', () => {
-					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					setData( model, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
 					insertHelper( '<blockWidget></blockWidget>' );
-					expect( getData( doc ) ).to.equal(
+					expect( getData( model ) ).to.equal(
 						'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>'
 					);
 				} );
@@ -587,12 +587,12 @@ describe( 'DataController', () => {
 
 		describe( 'filtering out', () => {
 			beforeEach( () => {
-				doc = new Document();
+				model = new Model();
+				doc = model.document;
 				doc.createRoot();
+				data = new DataController( model );
 
-				dataController = new DataController( doc );
-
-				const schema = doc.schema;
+				const schema = model.schema;
 
 				schema.registerItem( 'paragraph', '$block' );
 				schema.registerItem( 'heading1', '$block' );
@@ -624,80 +624,82 @@ describe( 'DataController', () => {
 			} );
 
 			it( 'filters out disallowed elements and leaves out the text', () => {
-				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				setData( model, '<paragraph>f[]oo</paragraph>' );
 				insertHelper( '<table><td>xxx</td><td>yyy</td></table>' );
-				expect( getData( doc ) ).to.equal( '<paragraph>fxxxyyy[]oo</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>fxxxyyy[]oo</paragraph>' );
 			} );
 
 			it( 'filters out disallowed elements and leaves out the paragraphs', () => {
-				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				setData( model, '<paragraph>f[]oo</paragraph>' );
 				insertHelper( '<table><td><paragraph>xxx</paragraph><paragraph>yyy</paragraph><paragraph>zzz</paragraph></td></table>' );
-				expect( getData( doc ) ).to.equal( '<paragraph>fxxx</paragraph><paragraph>yyy</paragraph><paragraph>zzz[]oo</paragraph>' );
+				expect( getData( model ) )
+					.to.equal( '<paragraph>fxxx</paragraph><paragraph>yyy</paragraph><paragraph>zzz[]oo</paragraph>' );
 			} );
 
 			it( 'filters out disallowed objects', () => {
-				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				setData( model, '<paragraph>f[]oo</paragraph>' );
 				insertHelper( '<disallowedWidget>xxx</disallowedWidget>' );
-				expect( getData( doc ) ).to.equal( '<paragraph>f[]oo</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>f[]oo</paragraph>' );
 			} );
 
 			it( 'filters out disallowed attributes when inserting text', () => {
-				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				setData( model, '<paragraph>f[]oo</paragraph>' );
 				insertHelper( 'x<$text a="1" b="1">x</$text>xy<$text a="1">y</$text>y' );
-				expect( getData( doc ) ).to.equal( '<paragraph>fx<$text b="1">x</$text>xyyy[]oo</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>fx<$text b="1">x</$text>xyyy[]oo</paragraph>' );
 			} );
 
 			it( 'filters out disallowed attributes when inserting nested elements', () => {
-				setData( doc, '<element>[]</element>' );
+				setData( model, '<element>[]</element>' );
 				insertHelper( '<table><td>f<$text a="1" b="1" c="1">o</$text>o</td></table>' );
-				expect( getData( doc ) ).to.equal( '<element><table><td>f<$text b="1">o</$text>o</td></table>[]</element>' );
+				expect( getData( model ) ).to.equal( '<element><table><td>f<$text b="1">o</$text>o</td></table>[]</element>' );
 			} );
 
 			it( 'filters out disallowed attributes when inserting text in disallowed elements', () => {
-				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				setData( model, '<paragraph>f[]oo</paragraph>' );
 				insertHelper( '<table><td>x<$text a="1" b="1">x</$text>x</td><td>y<$text a="1">y</$text>y</td></table>' );
-				expect( getData( doc ) ).to.equal( '<paragraph>fx<$text b="1">x</$text>xyyy[]oo</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>fx<$text b="1">x</$text>xyyy[]oo</paragraph>' );
 			} );
 
 			it( 'filters out disallowed attributes when merging #1', () => {
-				setData( doc, '<paragraph>[]foo</paragraph>' );
+				setData( model, '<paragraph>[]foo</paragraph>' );
 				insertHelper( '<paragraph>x<$text a="1" b="1">x</$text>x</paragraph>' );
-				expect( getData( doc ) ).to.equal( '<paragraph>x<$text b="1">x</$text>x[]foo</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>x<$text b="1">x</$text>x[]foo</paragraph>' );
 			} );
 
 			it( 'filters out disallowed attributes when merging #2', () => {
-				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				setData( model, '<paragraph>f[]oo</paragraph>' );
 				insertHelper( '<paragraph>x<$text a="1" b="1">x</$text>x</paragraph>' );
-				expect( getData( doc ) ).to.equal( '<paragraph>fx<$text b="1">x</$text>x[]oo</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>fx<$text b="1">x</$text>x[]oo</paragraph>' );
 			} );
 
 			it( 'filters out disallowed attributes when merging #3', () => {
-				setData( doc, '<paragraph>foo[]</paragraph>' );
+				setData( model, '<paragraph>foo[]</paragraph>' );
 				insertHelper( '<paragraph>x<$text a="1" b="1">x</$text>x</paragraph>' );
-				expect( getData( doc ) ).to.equal( '<paragraph>foox<$text b="1">x</$text>x[]</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>foox<$text b="1">x</$text>x[]</paragraph>' );
 			} );
 
 			it( 'filters out disallowed attributes from nested nodes when merging', () => {
-				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				setData( model, '<paragraph>f[]oo</paragraph>' );
 				insertHelper( '<heading1>x<element>b<$text a="1" b="1">a</$text>r</element>x</heading1>' );
-				expect( getData( doc ) ).to.equal( '<paragraph>fx<element>b<$text b="1">a</$text>r</element>x[]oo</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>fx<element>b<$text b="1">a</$text>r</element>x[]oo</paragraph>' );
 			} );
 
 			it( 'filters out disallowed attributes when autoparagraphing', () => {
-				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				setData( model, '<paragraph>f[]oo</paragraph>' );
 				insertHelper( '<paragraph>xxx</paragraph><$text a="1" b="1">yyy</$text>' );
-				expect( getData( doc ) ).to.equal( '<paragraph>fxxx</paragraph><paragraph><$text b="1">yyy[]</$text>oo</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>fxxx</paragraph><paragraph><$text b="1">yyy[]</$text>oo</paragraph>' );
 			} );
 		} );
 	} );
 
 	describe( 'integration with limit elements', () => {
 		beforeEach( () => {
-			doc = new Document();
+			model = new Model();
+			doc = model.document;
 			doc.createRoot();
-			dataController = new DataController( doc );
+			data = new DataController( model );
 
-			const schema = doc.schema;
+			const schema = model.schema;
 
 			schema.registerItem( 'limit' );
 			schema.allow( { name: 'limit', inside: '$root' } );
@@ -713,42 +715,42 @@ describe( 'DataController', () => {
 		it( 'should insert limit element', () => {
 			insertHelper( '<limit></limit>' );
 
-			expect( getData( doc ) ).to.equal( '<limit>[]</limit>' );
+			expect( getData( model ) ).to.equal( '<limit>[]</limit>' );
 		} );
 
 		it( 'should insert text into limit element', () => {
-			setData( doc, '<limit>[]</limit>' );
+			setData( model, '<limit>[]</limit>' );
 			insertHelper( 'foo bar' );
 
-			expect( getData( doc ) ).to.equal( '<limit>foo bar[]</limit>' );
+			expect( getData( model ) ).to.equal( '<limit>foo bar[]</limit>' );
 		} );
 
 		it( 'should insert text into limit element', () => {
-			setData( doc, '<limit>foo[</limit><limit>]bar</limit>' );
+			setData( model, '<limit>foo[</limit><limit>]bar</limit>' );
 			insertHelper( 'baz' );
 
-			expect( getData( doc ) ).to.equal( '<limit>foobaz[]</limit><limit>bar</limit>' );
+			expect( getData( model ) ).to.equal( '<limit>foobaz[]</limit><limit>bar</limit>' );
 		} );
 
 		it( 'should not insert disallowed elements inside limit elements', () => {
-			setData( doc, '<limit>[]</limit>' );
+			setData( model, '<limit>[]</limit>' );
 			insertHelper( '<disallowedElement></disallowedElement>' );
 
-			expect( getData( doc ) ).to.equal( '<limit>[]</limit>' );
+			expect( getData( model ) ).to.equal( '<limit>[]</limit>' );
 		} );
 
 		it( 'should not leave the limit element when inserting at the end', () => {
-			setData( doc, '<limit>foo[]</limit>' );
+			setData( model, '<limit>foo[]</limit>' );
 			insertHelper( '<paragraph>a</paragraph><paragraph>b</paragraph>' );
 
-			expect( getData( doc ) ).to.equal( '<limit>fooab[]</limit>' );
+			expect( getData( model ) ).to.equal( '<limit>fooab[]</limit>' );
 		} );
 
 		it( 'should not leave the limit element when inserting at the beginning', () => {
-			setData( doc, '<limit>[]foo</limit>' );
+			setData( model, '<limit>[]foo</limit>' );
 			insertHelper( '<paragraph>a</paragraph><paragraph>b</paragraph>' );
 
-			expect( getData( doc ) ).to.equal( '<limit>ab[]foo</limit>' );
+			expect( getData( model ) ).to.equal( '<limit>ab[]foo</limit>' );
 		} );
 	} );
 
@@ -756,14 +758,12 @@ 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, batch, {
+			content = parse( content, model.schema, {
 				context: [ '$clipboardHolder' ]
 			} );
 		}
 
-		insertContent( dataController, content, doc.selection, batch );
+		insertContent( data, content, doc.selection );
 	}
 } );

+ 109 - 108
packages/ckeditor5-engine/tests/controller/modifyselection.js

@@ -3,24 +3,25 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../src/model/document';
+import Model from '../../src/model/model';
 import DataController from '../../src/controller/datacontroller';
 import Selection from '../../src/model/selection';
 import modifySelection from '../../src/controller/modifyselection';
 import { setData, stringify } from '../../src/dev-utils/model';
 
-describe( 'DataController', () => {
-	let document, dataController;
+describe( 'DataController utils', () => {
+	let model, doc, data;
 
 	beforeEach( () => {
-		document = new Document();
-		dataController = new DataController( document );
-		document.schema.registerItem( 'p', '$block' );
-		document.schema.registerItem( 'x', '$block' );
+		model = new Model();
+		doc = model.document;
+		data = new DataController( model );
 
-		document.schema.allow( { name: 'x', inside: 'p' } );
+		model.schema.registerItem( 'p', '$block' );
+		model.schema.registerItem( 'x', '$block' );
+		model.schema.allow( { name: 'x', inside: 'p' } );
 
-		document.createRoot();
+		doc.createRoot();
 	} );
 
 	describe( 'modifySelection', () => {
@@ -65,12 +66,12 @@ describe( 'DataController', () => {
 				);
 
 				it( 'extends one character backward', () => {
-					setData( document, '<p>fo[]o</p>', { lastRangeBackward: true } );
+					setData( model, '<p>fo[]o</p>', { lastRangeBackward: true } );
 
-					modifySelection( dataController, document.selection, { direction: 'backward' } );
+					modifySelection( data, doc.selection, { direction: 'backward' } );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>f[o]o</p>' );
-					expect( document.selection.isBackward ).to.true;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>f[o]o</p>' );
+					expect( doc.selection.isBackward ).to.true;
 				} );
 
 				test(
@@ -80,12 +81,12 @@ describe( 'DataController', () => {
 				);
 
 				it( 'extends one character backward (non-collapsed)', () => {
-					setData( document, '<p>foob[a]r</p>', { lastRangeBackward: true } );
+					setData( model, '<p>foob[a]r</p>', { lastRangeBackward: true } );
 
-					modifySelection( dataController, document.selection, { direction: 'backward' } );
+					modifySelection( data, doc.selection, { direction: 'backward' } );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>foo[ba]r</p>' );
-					expect( document.selection.isBackward ).to.true;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foo[ba]r</p>' );
+					expect( doc.selection.isBackward ).to.true;
 				} );
 
 				test(
@@ -95,12 +96,12 @@ describe( 'DataController', () => {
 				);
 
 				it( 'extends to element boundary (backward)', () => {
-					setData( document, '<p>f[]oo</p>' );
+					setData( model, '<p>f[]oo</p>' );
 
-					modifySelection( dataController, document.selection, { direction: 'backward' } );
+					modifySelection( data, doc.selection, { direction: 'backward' } );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[f]oo</p>' );
-					expect( document.selection.isBackward ).to.true;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[f]oo</p>' );
+					expect( doc.selection.isBackward ).to.true;
 				} );
 
 				test(
@@ -111,12 +112,12 @@ describe( 'DataController', () => {
 				);
 
 				it( 'shrinks backward selection (to collapsed)', () => {
-					setData( document, '<p>foo[b]ar</p>', { lastRangeBackward: true } );
+					setData( model, '<p>foo[b]ar</p>', { lastRangeBackward: true } );
 
-					modifySelection( dataController, document.selection );
+					modifySelection( data, doc.selection );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>foob[]ar</p>' );
-					expect( document.selection.isBackward ).to.false;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foob[]ar</p>' );
+					expect( doc.selection.isBackward ).to.false;
 				} );
 
 				test(
@@ -126,12 +127,12 @@ describe( 'DataController', () => {
 				);
 
 				it( 'unicode support - combining mark backward', () => {
-					setData( document, '<p>foob̂[]ar</p>' );
+					setData( model, '<p>foob̂[]ar</p>' );
 
-					modifySelection( dataController, document.selection, { direction: 'backward' } );
+					modifySelection( data, doc.selection, { direction: 'backward' } );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>foo[b̂]ar</p>' );
-					expect( document.selection.isBackward ).to.true;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foo[b̂]ar</p>' );
+					expect( doc.selection.isBackward ).to.true;
 				} );
 
 				test(
@@ -141,12 +142,12 @@ describe( 'DataController', () => {
 				);
 
 				it( 'unicode support - combining mark multiple backward', () => {
-					setData( document, '<p>foo̻̐ͩ[]bar</p>' );
+					setData( model, '<p>foo̻̐ͩ[]bar</p>' );
 
-					modifySelection( dataController, document.selection, { direction: 'backward' } );
+					modifySelection( data, doc.selection, { direction: 'backward' } );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>fo[o̻̐ͩ]bar</p>' );
-					expect( document.selection.isBackward ).to.true;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>fo[o̻̐ͩ]bar</p>' );
+					expect( doc.selection.isBackward ).to.true;
 				} );
 
 				test(
@@ -162,12 +163,12 @@ describe( 'DataController', () => {
 				);
 
 				it( 'unicode support - surrogate pairs backward', () => {
-					setData( document, '<p>\uD83D\uDCA9[]</p>' );
+					setData( model, '<p>\uD83D\uDCA9[]</p>' );
 
-					modifySelection( dataController, document.selection, { direction: 'backward' } );
+					modifySelection( data, doc.selection, { direction: 'backward' } );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
-					expect( document.selection.isBackward ).to.true;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
+					expect( doc.selection.isBackward ).to.true;
 				} );
 			} );
 
@@ -179,12 +180,12 @@ describe( 'DataController', () => {
 				);
 
 				it( 'extends over boundary of empty elements (backward)', () => {
-					setData( document, '<p></p><p></p><p>[]</p>' );
+					setData( model, '<p></p><p></p><p>[]</p>' );
 
-					modifySelection( dataController, document.selection, { direction: 'backward' } );
+					modifySelection( data, doc.selection, { direction: 'backward' } );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p></p><p>[</p><p>]</p>' );
-					expect( document.selection.isBackward ).to.true;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p></p><p>[</p><p>]</p>' );
+					expect( doc.selection.isBackward ).to.true;
 				} );
 
 				test(
@@ -194,12 +195,12 @@ describe( 'DataController', () => {
 				);
 
 				it( 'extends over boundary of non-empty elements (backward)', () => {
-					setData( document, '<p>a</p><p>[]bcd</p>' );
+					setData( model, '<p>a</p><p>[]bcd</p>' );
 
-					modifySelection( dataController, document.selection, { direction: 'backward' } );
+					modifySelection( data, doc.selection, { direction: 'backward' } );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>a[</p><p>]bcd</p>' );
-					expect( document.selection.isBackward ).to.true;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>a[</p><p>]bcd</p>' );
+					expect( doc.selection.isBackward ).to.true;
 				} );
 
 				test(
@@ -209,12 +210,12 @@ describe( 'DataController', () => {
 				);
 
 				it( 'extends over character after boundary (backward)', () => {
-					setData( document, '<p>abc[</p><p>]d</p>', { lastRangeBackward: true } );
+					setData( model, '<p>abc[</p><p>]d</p>', { lastRangeBackward: true } );
 
-					modifySelection( dataController, document.selection, { direction: 'backward' } );
+					modifySelection( data, doc.selection, { direction: 'backward' } );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>ab[c</p><p>]d</p>' );
-					expect( document.selection.isBackward ).to.true;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>ab[c</p><p>]d</p>' );
+					expect( doc.selection.isBackward ).to.true;
 				} );
 
 				test(
@@ -243,30 +244,30 @@ describe( 'DataController', () => {
 				);
 
 				it( 'shrinks over boundary of empty elements', () => {
-					setData( document, '<p>[</p><p>]</p>', { lastRangeBackward: true } );
+					setData( model, '<p>[</p><p>]</p>', { lastRangeBackward: true } );
 
-					modifySelection( dataController, document.selection );
+					modifySelection( data, doc.selection );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p></p><p>[]</p>' );
-					expect( document.selection.isBackward ).to.false;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p></p><p>[]</p>' );
+					expect( doc.selection.isBackward ).to.false;
 				} );
 
 				it( 'shrinks over boundary of empty elements (backward)', () => {
-					setData( document, '<p>[</p><p>]</p>' );
+					setData( model, '<p>[</p><p>]</p>' );
 
-					modifySelection( dataController, document.selection, { direction: 'backward' } );
+					modifySelection( data, doc.selection, { direction: 'backward' } );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[]</p><p></p>' );
-					expect( document.selection.isBackward ).to.false;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[]</p><p></p>' );
+					expect( doc.selection.isBackward ).to.false;
 				} );
 
 				it( 'shrinks over boundary of non-empty elements', () => {
-					setData( document, '<p>a[</p><p>]b</p>', { lastRangeBackward: true } );
+					setData( model, '<p>a[</p><p>]b</p>', { lastRangeBackward: true } );
 
-					modifySelection( dataController, document.selection );
+					modifySelection( data, doc.selection );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>a</p><p>[]b</p>' );
-					expect( document.selection.isBackward ).to.false;
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>a</p><p>[]b</p>' );
+					expect( doc.selection.isBackward ).to.false;
 				} );
 
 				test(
@@ -277,20 +278,20 @@ describe( 'DataController', () => {
 				);
 
 				it( 'updates selection attributes', () => {
-					setData( document, '<p><$text bold="true">foo</$text>[b]</p>' );
+					setData( model, '<p><$text bold="true">foo</$text>[b]</p>' );
 
-					modifySelection( dataController, document.selection, { direction: 'backward' } );
+					modifySelection( data, doc.selection, { direction: 'backward' } );
 
-					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p><$text bold="true">foo[]</$text>b</p>' );
-					expect( document.selection.getAttribute( 'bold' ) ).to.equal( true );
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p><$text bold="true">foo[]</$text>b</p>' );
+					expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
 				} );
 			} );
 
 			describe( 'beyond element – skipping incorrect positions', () => {
 				beforeEach( () => {
-					document.schema.registerItem( 'quote' );
-					document.schema.allow( { name: 'quote', inside: '$root' } );
-					document.schema.allow( { name: '$block', inside: 'quote' } );
+					model.schema.registerItem( 'quote' );
+					model.schema.allow( { name: 'quote', inside: '$root' } );
+					model.schema.allow( { name: '$block', inside: 'quote' } );
 				} );
 
 				test(
@@ -336,13 +337,13 @@ describe( 'DataController', () => {
 
 		describe( 'unit=codePoint', () => {
 			it( 'does nothing on empty content', () => {
-				document.schema.allow( { name: '$text', inside: '$root' } );
+				model.schema.allow( { name: '$text', inside: '$root' } );
 
-				setData( document, '' );
+				setData( model, '' );
 
-				modifySelection( dataController, document.selection, { unit: 'codePoint' } );
+				modifySelection( data, doc.selection, { unit: 'codePoint' } );
 
-				expect( stringify( document.getRoot(), document.selection ) ).to.equal( '[]' );
+				expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '[]' );
 			} );
 
 			test(
@@ -360,12 +361,12 @@ describe( 'DataController', () => {
 			);
 
 			it( 'extends one user-perceived character backward - latin letters', () => {
-				setData( document, '<p>fo[]o</p>' );
+				setData( model, '<p>fo[]o</p>' );
 
-				modifySelection( dataController, document.selection, { unit: 'codePoint', direction: 'backward' } );
+				modifySelection( data, doc.selection, { unit: 'codePoint', direction: 'backward' } );
 
-				expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>f[o]o</p>' );
-				expect( document.selection.isBackward ).to.true;
+				expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>f[o]o</p>' );
+				expect( doc.selection.isBackward ).to.true;
 			} );
 
 			test(
@@ -376,15 +377,15 @@ describe( 'DataController', () => {
 			);
 
 			it( 'unicode support - combining mark backward', () => {
-				setData( document, '<p>foob̂[]ar</p>' );
+				setData( model, '<p>foob̂[]ar</p>' );
 
 				// Creating new instance of selection instead of operation on module:engine/model/document~Document#selection.
 				// Document's selection will throw errors in some test cases (which are correct cases, but only for
 				// non-document selections).
-				const testSelection = Selection.createFromSelection( document.selection );
-				modifySelection( dataController, testSelection, { unit: 'codePoint', direction: 'backward' } );
+				const testSelection = Selection.createFromSelection( doc.selection );
+				modifySelection( data, testSelection, { unit: 'codePoint', direction: 'backward' } );
 
-				expect( stringify( document.getRoot(), testSelection ) ).to.equal( '<p>foob[̂]ar</p>' );
+				expect( stringify( doc.getRoot(), testSelection ) ).to.equal( '<p>foob[̂]ar</p>' );
 				expect( testSelection.isBackward ).to.true;
 			} );
 
@@ -403,25 +404,25 @@ describe( 'DataController', () => {
 			);
 
 			it( 'unicode support surrogate pairs backward', () => {
-				setData( document, '<p>\uD83D\uDCA9[]</p>' );
+				setData( model, '<p>\uD83D\uDCA9[]</p>' );
 
-				modifySelection( dataController, document.selection, { unit: 'codePoint', direction: 'backward' } );
+				modifySelection( data, doc.selection, { unit: 'codePoint', direction: 'backward' } );
 
-				expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
-				expect( document.selection.isBackward ).to.true;
+				expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
+				expect( doc.selection.isBackward ).to.true;
 			} );
 		} );
 
 		describe( 'objects handling', () => {
 			beforeEach( () => {
-				document.schema.registerItem( 'obj' );
-				document.schema.allow( { name: 'obj', inside: '$root' } );
-				document.schema.allow( { name: '$text', inside: 'obj' } );
-				document.schema.objects.add( 'obj' );
-
-				document.schema.registerItem( 'inlineObj', '$inline' );
-				document.schema.allow( { name: '$text', inside: 'inlineObj' } );
-				document.schema.objects.add( 'inlineObj' );
+				model.schema.registerItem( 'obj' );
+				model.schema.allow( { name: 'obj', inside: '$root' } );
+				model.schema.allow( { name: '$text', inside: 'obj' } );
+				model.schema.objects.add( 'obj' );
+
+				model.schema.registerItem( 'inlineObj', '$inline' );
+				model.schema.allow( { name: '$text', inside: 'inlineObj' } );
+				model.schema.objects.add( 'inlineObj' );
 			} );
 
 			test(
@@ -444,12 +445,12 @@ describe( 'DataController', () => {
 			);
 
 			it( 'extends over object elements - backward', () => {
-				setData( document, '<obj></obj>[<obj></obj>]', { lastRangeBackward: true } );
+				setData( model, '<obj></obj>[<obj></obj>]', { lastRangeBackward: true } );
 
-				modifySelection( dataController, document.selection, { direction: 'backward' } );
+				modifySelection( data, doc.selection, { direction: 'backward' } );
 
-				expect( stringify( document.getRoot(), document.selection ) ).to.equal( '[<obj></obj><obj></obj>]' );
-				expect( document.selection.isBackward ).to.true;
+				expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '[<obj></obj><obj></obj>]' );
+				expect( doc.selection.isBackward ).to.true;
 			} );
 
 			test(
@@ -481,16 +482,16 @@ describe( 'DataController', () => {
 
 		describe( 'limits handling', () => {
 			beforeEach( () => {
-				document.schema.registerItem( 'inlineLimit' );
-				document.schema.allow( { name: 'inlineLimit', inside: '$block' } );
-				document.schema.allow( { name: '$text', inside: 'inlineLimit' } );
+				model.schema.registerItem( 'inlineLimit' );
+				model.schema.allow( { name: 'inlineLimit', inside: '$block' } );
+				model.schema.allow( { name: '$text', inside: 'inlineLimit' } );
 
-				document.schema.registerItem( 'blockLimit' );
-				document.schema.allow( { name: 'blockLimit', inside: '$root' } );
-				document.schema.allow( { name: 'p', inside: 'blockLimit' } );
+				model.schema.registerItem( 'blockLimit' );
+				model.schema.allow( { name: 'blockLimit', inside: '$root' } );
+				model.schema.allow( { name: 'p', inside: 'blockLimit' } );
 
-				document.schema.limits.add( 'inlineLimit' );
-				document.schema.limits.add( 'blockLimit' );
+				model.schema.limits.add( 'inlineLimit' );
+				model.schema.limits.add( 'blockLimit' );
 			} );
 
 			test(
@@ -542,15 +543,15 @@ describe( 'DataController', () => {
 			input = input.normalize();
 			output = output.normalize();
 
-			setData( document, input );
+			setData( model, input );
 
 			// Creating new instance of selection instead of operation on module:engine/model/document~Document#selection.
 			// Document's selection will throw errors in some test cases (which are correct cases, but only for
 			// non-document selections).
-			const testSelection = Selection.createFromSelection( document.selection );
-			modifySelection( dataController, testSelection, options );
+			const testSelection = Selection.createFromSelection( doc.selection );
+			modifySelection( data, testSelection, options );
 
-			expect( stringify( document.getRoot(), testSelection ) ).to.equal( output );
+			expect( stringify( doc.getRoot(), testSelection ) ).to.equal( output );
 		} );
 	}
 } );

+ 29 - 24
packages/ckeditor5-engine/tests/conversion/advanced-converters.js

@@ -3,13 +3,15 @@
  * For licensing, see LICENSE.md.
  */
 
-import ModelDocument from '../../src/model/document';
+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 ModelWriter from '../../src/model/writer';
+import Batch from '../../src/model/batch';
 
 import ViewElement from '../../src/view/element';
 import ViewContainerElement from '../../src/view/containerelement';
@@ -38,25 +40,28 @@ 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, batch;
+	let model, modelDoc, modelRoot, viewRoot, mapper, modelDispatcher, viewDispatcher, modelWriter;
 
 	beforeEach( () => {
-		modelDoc = new ModelDocument();
+		model = new Model();
+		modelDoc = model.document;
 		modelRoot = modelDoc.createRoot();
 		viewRoot = new ViewContainerElement( 'div' );
-		batch = modelDoc.batch();
 
 		mapper = new Mapper();
 		mapper.bindElements( modelRoot, viewRoot );
 
-		modelDispatcher = new ModelConversionDispatcher( modelDoc, { mapper } );
+		modelDispatcher = new ModelConversionDispatcher( model, { mapper } );
 		// Schema is mocked up because we don't care about it in those tests.
-		viewDispatcher = new ViewConversionDispatcher( { schema: { check: () => true } } );
+		viewDispatcher = new ViewConversionDispatcher( model, { schema: { check: () => true } } );
 
 		modelDispatcher.on( 'insert:$text', insertText() );
 		modelDispatcher.on( 'remove', remove() );
 		viewDispatcher.on( 'text', convertText() );
 		viewDispatcher.on( 'documentFragment', convertToModelFragment() );
+
+		// We need to create a model writer to modify model tree in tests.
+		modelWriter = new ModelWriter( model, new Batch() );
 	} );
 
 	function viewAttributesToString( item ) {
@@ -217,16 +222,16 @@ describe( 'advanced-converters', () => {
 
 			const viewImageConverter = function( evt, data, consumable, conversionApi ) {
 				if ( consumable.consume( data.input, { name: true } ) ) {
-					data.output = conversionApi.batch.createElement( 'image', data.input.getAttributes() );
+					data.output = conversionApi.writer.createElement( 'image', data.input.getAttributes() );
 				}
 			};
 
 			const viewFigcaptionConverter = function( evt, data, consumable, conversionApi ) {
 				if ( consumable.consume( data.input, { name: true } ) ) {
-					const modelCaption = conversionApi.batch.createElement( 'caption' );
+					const modelCaption = conversionApi.writer.createElement( 'caption' );
 					const children = conversionApi.convertChildren( data.input, consumable );
 
-					conversionApi.batch.append( children, modelCaption );
+					conversionApi.writer.append( children, modelCaption );
 
 					data.output = modelCaption;
 				}
@@ -242,14 +247,14 @@ describe( 'advanced-converters', () => {
 		} );
 
 		it( 'should convert model images changes without caption to view', () => {
-			const modelElement = batch.createElement( 'image', { src: 'bar.jpg', title: 'bar' } );
-			batch.append( modelElement, modelRoot );
+			const modelElement = new ModelElement( 'image', { src: 'bar.jpg', title: 'bar' } );
+			modelRoot.appendChildren( modelElement );
 			modelDispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><img src="bar.jpg" title="bar"></img></div>' );
 
-			batch.setAttribute( 'src', 'new.jpg', modelElement );
-			batch.removeAttribute( 'title', modelElement );
+			modelElement.setAttribute( 'src', 'new.jpg' );
+			modelElement.removeAttribute( 'title' );
 			modelDispatcher.convertAttribute( 'changeAttribute', createRangeOnElementOnly( modelElement ), 'src', 'bar.jpg', 'new.jpg' );
 			modelDispatcher.convertAttribute( 'removeAttribute', createRangeOnElementOnly( modelElement ), 'title', 'bar', null );
 
@@ -260,7 +265,7 @@ describe( 'advanced-converters', () => {
 			const modelElement = new ModelElement( 'image', { src: 'foo.jpg', title: 'foo' }, [
 				new ModelElement( 'caption', {}, new ModelText( 'foobar' ) )
 			] );
-			modelRoot.appendChildren( modelElement );
+			modelRoot.appendChildren( [ modelElement ] );
 			modelDispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 
 			expect( viewToString( viewRoot ) ).to.equal(
@@ -279,7 +284,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, batch );
+			const modelElement = viewDispatcher.convert( viewElement );
 
 			expect( modelToString( modelElement ) ).to.equal( '<image src="bar.jpg" title="bar"></image>' );
 		} );
@@ -293,7 +298,7 @@ describe( 'advanced-converters', () => {
 					new ViewContainerElement( 'figcaption', null, new ViewText( 'foobar' ) )
 				]
 			);
-			const modelElement = viewDispatcher.convert( viewElement, batch );
+			const modelElement = viewDispatcher.convert( viewElement );
 
 			expect( modelToString( modelElement ) ).to.equal( '<image src="bar.jpg" title="bar"><caption>foobar</caption></image>' );
 		} );
@@ -478,7 +483,7 @@ describe( 'advanced-converters', () => {
 			expect( viewToString( viewRoot ) ).to.equal( '<div><a href="foo.html" title="Foo title">foo</a></div>' );
 
 			// Let's change link's attributes.
-			batch.setAttributes( {
+			modelWriter.setAttributes( {
 				linkHref: 'bar.html',
 				linkTitle: 'Bar title'
 			}, range );
@@ -487,7 +492,7 @@ describe( 'advanced-converters', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><a href="bar.html" title="Bar title">foo</a></div>' );
 
-			batch.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 ) );
+			modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 ) );
 			modelDispatcher.convertRemove(
 				ModelPosition.createFromParentAndOffset( modelRoot, 0 ),
 				ModelRange.createIn( modelDoc.graveyard )
@@ -498,13 +503,13 @@ describe( 'advanced-converters', () => {
 			range = ModelRange.createIn( modelRoot );
 
 			// Let's remove just one attribute.
-			batch.removeAttribute( 'linkTitle', range );
+			modelWriter.removeAttribute( 'linkTitle', range );
 			modelDispatcher.convertAttribute( 'removeAttribute', range, 'linkTitle', 'Bar title', null );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><a href="bar.html">oo</a></div>' );
 
 			// Let's remove the other attribute.
-			batch.removeAttribute( 'linkHref', range );
+			modelWriter.removeAttribute( 'linkHref', range );
 			modelDispatcher.convertAttribute( 'removeAttribute', range, 'linkHref', 'bar.html', null );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div>oo</div>' );
@@ -513,7 +518,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, batch ).getChild( 0 );
+			const modelText = viewDispatcher.convert( viewElement ).getChild( 0 );
 
 			expect( modelText ).to.be.instanceof( ModelText );
 			expect( modelText.data ).to.equal( 'foo' );
@@ -584,7 +589,7 @@ describe( 'advanced-converters', () => {
 				]
 			);
 
-			const modelElement = viewDispatcher.convert( viewElement, batch );
+			const modelElement = viewDispatcher.convert( viewElement );
 
 			expect( modelToString( modelElement ) ).to.equal( '<quote linkHref="foo.html" linkTitle="Foo source">foo</quote>' );
 		} );
@@ -647,7 +652,7 @@ describe( 'advanced-converters', () => {
 			] )
 		] );
 
-		expect( modelToString( viewDispatcher.convert( viewTable, batch ) ) )
+		expect( modelToString( viewDispatcher.convert( viewTable ) ) )
 			.to.equal( '<paragraph>foo <$text linkHref="bar.html">bar</$text></paragraph><paragraph>abc</paragraph>' );
 	} );
 
@@ -746,7 +751,7 @@ describe( 'advanced-converters', () => {
 				] )
 			] );
 
-			const modelElement = viewDispatcher.convert( viewElement, batch );
+			const modelElement = viewDispatcher.convert( viewElement );
 
 			expect( modelToString( modelElement ) ).to.equal(
 				'<table cellpadding="5" cellspacing="5">' +

+ 9 - 10
packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js

@@ -5,7 +5,7 @@
 
 import buildModelConverter from '../../src/conversion/buildmodelconverter';
 
-import ModelDocument from '../../src/model/document';
+import Model from '../../src/model/model';
 import ModelElement from '../../src/model/element';
 import ModelText from '../../src/model/text';
 import ModelRange from '../../src/model/range';
@@ -69,14 +69,13 @@ function viewToString( item ) {
 }
 
 describe( 'Model converter builder', () => {
-	let dispatcher, mapper, modelDoc, modelRoot, viewDoc, viewRoot, viewSelection, batch;
+	let dispatcher, mapper, modelDoc, modelRoot, viewDoc, viewRoot, viewSelection, model;
 
 	beforeEach( () => {
-		modelDoc = new ModelDocument();
+		model = new Model();
+		modelDoc = model.document;
 		modelRoot = modelDoc.createRoot( 'root', 'root' );
 
-		batch = modelDoc.batch();
-
 		viewDoc = new ViewDocument();
 		viewRoot = viewDoc.createRoot( 'div' );
 		viewSelection = viewDoc.selection;
@@ -84,7 +83,7 @@ describe( 'Model converter builder', () => {
 		mapper = new Mapper();
 		mapper.bindElements( modelRoot, viewRoot );
 
-		dispatcher = new ModelConversionDispatcher( modelDoc, { mapper, viewSelection } );
+		dispatcher = new ModelConversionDispatcher( model, { mapper, viewSelection } );
 
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'remove', remove() );
@@ -146,7 +145,7 @@ describe( 'Model converter builder', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><strong>foo</strong></div>' );
 
-			batch.removeAttribute( 'bold', modelRoot );
+			modelRoot.removeAttribute( 'bold' );
 
 			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelRoot ), 'bold', true, null );
 
@@ -163,7 +162,7 @@ describe( 'Model converter builder', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><strong>foo</strong></div>' );
 
-			batch.removeAttribute( 'bold', modelRoot );
+			modelRoot.removeAttribute( 'bold' );
 
 			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelRoot ), 'bold', true, null );
 
@@ -180,13 +179,13 @@ describe( 'Model converter builder', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );
 
-			batch.setAttribute( 'italic', 'i', modelRoot );
+			modelRoot.setAttribute( 'italic', 'i' );
 
 			dispatcher.convertAttribute( 'changeAttribute', ModelRange.createIn( modelRoot ), 'italic', 'em', 'i' );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><i>foo</i></div>' );
 
-			batch.removeAttribute( 'italic', modelRoot );
+			modelRoot.removeAttribute( 'italic' );
 
 			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelRoot ), 'italic', 'i', null );
 

+ 32 - 37
packages/ckeditor5-engine/tests/conversion/buildviewconverter.js

@@ -5,8 +5,8 @@
 
 import buildViewConverter from '../../src/conversion/buildviewconverter';
 
+import Model from '../../src/model/model';
 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';
@@ -64,13 +64,11 @@ const textAttributes = [ undefined, 'linkHref', 'linkTitle', 'bold', 'italic', '
 const pAttributes = [ undefined, 'class', 'important', 'theme', 'decorated', 'size' ];
 
 describe( 'View converter builder', () => {
-	let dispatcher, schema, additionalData, batch;
+	let dispatcher, schema, additionalData;
 
-	const modelDocument = new ModelDocument();
+	const model = new Model();
 
 	beforeEach( () => {
-		batch = modelDocument.batch();
-
 		// `additionalData` parameter for `.convert` calls.
 		additionalData = { context: [ '$root' ] };
 
@@ -93,14 +91,14 @@ describe( 'View converter builder', () => {
 		schema.allow( { name: 'span', attributes: [ 'transformer' ], inside: '$root' } );
 		schema.allow( { name: 'div', attributes: [ 'class' ], inside: '$root' } );
 
-		dispatcher = new ViewConversionDispatcher( { schema } );
+		dispatcher = new ViewConversionDispatcher( model, { schema } );
 		dispatcher.on( 'text', convertText() );
 	} );
 
 	it( 'should convert from view element to model element', () => {
 		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
 
-		const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), batch, additionalData );
+		const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
@@ -110,7 +108,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' } ), batch, additionalData );
+		const conversionResult = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<image src="foo.jpg"></image>' );
 	} );
@@ -119,7 +117,7 @@ describe( 'View converter builder', () => {
 		buildViewConverter().for( dispatcher ).fromElement( 'strong' ).toAttribute( 'bold', true );
 
 		const conversionResult = dispatcher.convert(
-			new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData
+			new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
 		);
 
 		// Have to check root because result is a ModelText.
@@ -132,7 +130,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' ) ), batch, additionalData
+			new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		// Have to check root because result is a ModelText.
@@ -147,7 +145,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' ) ), batch, additionalData
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
@@ -169,7 +167,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'p', { 'data-type': 'foo' }, new ViewText( 'xyz' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewStructure, batch, additionalData );
+		const conversionResult = dispatcher.convert( viewStructure, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal(
 			'<paragraph important="true">foo</paragraph>' +
@@ -195,7 +193,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'span', { style: 'font-weight:bold; font-size:20px' }, new ViewText( 'ddd' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph><$text bold="true">aaabbbcccddd</$text></paragraph>' );
 	} );
@@ -212,7 +210,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		const markerSearch = conversionResult.markers.get( 'search' );
 
@@ -234,7 +232,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		const markerSearch = conversionResult.markers.get( 'search' );
 
@@ -260,7 +258,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'r' )
 		] );
 
-		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		const marker1 = conversionResult.markers.get( 'marker1' );
 		const marker2 = conversionResult.markers.get( 'marker2' );
@@ -277,7 +275,7 @@ describe( 'View converter builder', () => {
 
 		const element = new ViewAttributeElement( 'span' );
 
-		const result = dispatcher.convert( element, batch, additionalData );
+		const result = dispatcher.convert( element, additionalData );
 
 		expect( result ).to.be.instanceof( ModelDocumentFragment );
 		expect( result.childCount ).to.equal( 0 );
@@ -289,7 +287,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { class: 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, batch, additionalData );
+			dispatcher.convert( element, additionalData );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -301,7 +299,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, batch, additionalData );
+			dispatcher.convert( element, additionalData );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -313,7 +311,7 @@ describe( 'View converter builder', () => {
 		const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } );
 
 		expect( () => {
-			dispatcher.convert( element, batch, additionalData );
+			dispatcher.convert( element, additionalData );
 		} ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ );
 	} );
 
@@ -330,7 +328,7 @@ describe( 'View converter builder', () => {
 
 		// Not quite megatron.
 		result = dispatcher.convert(
-			new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), batch, additionalData
+			new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<span>foo</span>' );
@@ -338,7 +336,6 @@ 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' ) ),
-			batch,
 			additionalData
 		);
 
@@ -351,7 +348,6 @@ describe( 'View converter builder', () => {
 				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
 				new ViewText( 'foo' )
 			),
-			batch,
 			additionalData
 		);
 
@@ -364,7 +360,6 @@ describe( 'View converter builder', () => {
 				{ class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' },
 				new ViewText( 'foo' )
 			),
-			batch,
 			additionalData
 		);
 
@@ -385,7 +380,7 @@ describe( 'View converter builder', () => {
 			new ViewText( 'foo' )
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<span transformer="megatron">foo</span>' );
 	} );
@@ -395,7 +390,7 @@ describe( 'View converter builder', () => {
 
 		const viewElement = new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) );
 
-		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( conversionResult.is( 'documentFragment' ) ).to.be.true;
 	} );
@@ -407,7 +402,7 @@ describe( 'View converter builder', () => {
 		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
 
 		const conversionResult = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), batch, additionalData
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		// Element converter was fired first even though attribute converter was added first.
@@ -423,7 +418,7 @@ describe( 'View converter builder', () => {
 		let result;
 
 		result = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), batch, additionalData
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<paragraph class="myClass">foo</paragraph>' );
@@ -433,7 +428,7 @@ describe( 'View converter builder', () => {
 			.toElement( 'customP' );
 
 		result = dispatcher.convert(
-			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), batch, additionalData
+			new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData
 		);
 
 		expect( modelToString( result ) ).to.equal( '<customP>foo</customP>' );
@@ -454,7 +449,7 @@ describe( 'View converter builder', () => {
 			.toAttribute( 'size', 'small' );
 
 		const viewElement = new ViewContainerElement( 'p', { class: 'decorated small' }, new ViewText( 'foo' ) );
-		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		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
@@ -477,7 +472,7 @@ describe( 'View converter builder', () => {
 			new ViewContainerElement( 'abcd', null, new ViewText( 'foo' ) )
 		] );
 
-		const conversionResult = dispatcher.convert( viewStructure, batch, additionalData );
+		const conversionResult = dispatcher.convert( viewStructure, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<div class="myClass"><abcd>foo</abcd></div>' );
 	} );
@@ -496,7 +491,7 @@ describe( 'View converter builder', () => {
 			)
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
@@ -515,7 +510,7 @@ describe( 'View converter builder', () => {
 			)
 		);
 
-		const conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		const conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph>foo</paragraph>' );
 	} );
@@ -529,11 +524,11 @@ describe( 'View converter builder', () => {
 			} );
 
 		const viewElement = new ViewContainerElement( 'p' );
-		let conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		let conversionResult = dispatcher.convert( viewElement, additionalData );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph></paragraph>' );
 
 		viewElement.setAttribute( 'stop', true );
-		conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		conversionResult = dispatcher.convert( viewElement, additionalData );
 
 		expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 		expect( conversionResult.childCount ).to.equal( 0 );
@@ -551,11 +546,11 @@ describe( 'View converter builder', () => {
 		} );
 
 		const viewElement = new ViewContainerElement( 'p', { 'data-type': 'foo' } );
-		let conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		let conversionResult = dispatcher.convert( viewElement, additionalData );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph type="foo"></paragraph>' );
 
 		viewElement.setAttribute( 'data-type', 'stop' );
-		conversionResult = dispatcher.convert( viewElement, batch, additionalData );
+		conversionResult = dispatcher.convert( viewElement, additionalData );
 		expect( modelToString( conversionResult ) ).to.equal( '<paragraph></paragraph>' );
 	} );
 } );

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

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import ModelDocument from '../../src/model/document';
+import Model from '../../src/model/model';
 import ModelElement from '../../src/model/element';
 import ModelRange from '../../src/model/range';
 import ModelPosition from '../../src/model/position';
@@ -37,14 +37,15 @@ import { stringify as stringifyView } from '../../src/dev-utils/view';
 import { setData as setModelData } from '../../src/dev-utils/model';
 
 describe( 'model-selection-to-view-converters', () => {
-	let dispatcher, mapper, modelDoc, modelRoot, modelSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor;
+	let dispatcher, mapper, model, modelDoc, modelRoot, modelSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor;
 
 	beforeEach( () => {
-		modelDoc = new ModelDocument();
+		model = new Model();
+		modelDoc = model.document;
 		modelRoot = modelDoc.createRoot();
 		modelSelection = modelDoc.selection;
 
-		modelDoc.schema.allow( { name: '$text', inside: '$root' } );
+		model.schema.allow( { name: '$text', inside: '$root' } );
 
 		viewDoc = new ViewDocument();
 		viewRoot = viewDoc.createRoot( 'div' );
@@ -55,7 +56,7 @@ describe( 'model-selection-to-view-converters', () => {
 
 		highlightDescriptor = { class: 'marker', priority: 1 };
 
-		dispatcher = new ModelConversionDispatcher( modelDoc, { mapper, viewSelection } );
+		dispatcher = new ModelConversionDispatcher( model, { mapper, viewSelection } );
 
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
@@ -218,8 +219,8 @@ describe( 'model-selection-to-view-converters', () => {
 			} );
 
 			it( 'in attribute and marker', () => {
-				setModelData( modelDoc, 'fo<$text bold="true">ob</$text>ar' );
-				const marker = modelDoc.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
+				setModelData( model, 'fo<$text bold="true">ob</$text>ar' );
+				const marker = model.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
 
 				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
 
@@ -233,7 +234,7 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 				dispatcher.convertMarker( 'addMarker', marker.name, marker.getRange() );
 
-				const markers = Array.from( modelDoc.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
+				const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
 				dispatcher.convertSelection( modelSelection, markers );
 
 				// Stringify view and check if it is same as expected.
@@ -243,8 +244,8 @@ describe( 'model-selection-to-view-converters', () => {
 			} );
 
 			it( 'in attribute and marker - no attribute', () => {
-				setModelData( modelDoc, 'fo<$text bold="true">ob</$text>ar' );
-				const marker = modelDoc.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
+				setModelData( model, 'fo<$text bold="true">ob</$text>ar' );
+				const marker = model.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
 
 				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
 
@@ -260,7 +261,7 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 				dispatcher.convertMarker( 'addMarker', marker.name, marker.getRange() );
 
-				const markers = Array.from( modelDoc.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
+				const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
 				dispatcher.convertSelection( modelSelection, markers );
 
 				// Stringify view and check if it is same as expected.
@@ -273,8 +274,8 @@ describe( 'model-selection-to-view-converters', () => {
 					data => ( { 'class': data.markerName } )
 				) );
 
-				setModelData( modelDoc, 'foobar' );
-				const marker = modelDoc.markers.set( 'marker2', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
+				setModelData( model, 'foobar' );
+				const marker = model.markers.set( 'marker2', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
 
 				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
 
@@ -285,7 +286,7 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 				dispatcher.convertMarker( 'addMarker', marker.name, marker.getRange() );
 
-				const markers = Array.from( modelDoc.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
+				const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
 				dispatcher.convertSelection( modelSelection, markers );
 
 				// Stringify view and check if it is same as expected.
@@ -297,8 +298,8 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.on( 'addMarker:marker2', highlightText( data => ( { 'class': data.markerName } ) ) );
 				dispatcher.on( 'selectionMarker:marker2', convertSelectionMarker( data => ( { 'class': data.markerName } ) ) );
 
-				setModelData( modelDoc, 'foobar' );
-				const marker = modelDoc.markers.set( 'marker2', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
+				setModelData( model, 'foobar' );
+				const marker = model.markers.set( 'marker2', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
 
 				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
 
@@ -309,7 +310,7 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 				dispatcher.convertMarker( 'addMarker', marker.name, marker.getRange() );
 
-				const markers = Array.from( modelDoc.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
+				const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
 				dispatcher.convertSelection( modelSelection, markers );
 
 				// Stringify view and check if it is same as expected.
@@ -322,8 +323,8 @@ describe( 'model-selection-to-view-converters', () => {
 
 				} ) );
 
-				setModelData( modelDoc, 'foobar' );
-				const marker = modelDoc.markers.set( 'marker3', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
+				setModelData( model, 'foobar' );
+				const marker = model.markers.set( 'marker3', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
 
 				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
 
@@ -334,7 +335,7 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 				dispatcher.convertMarker( 'addMarker', marker.name, marker.getRange() );
 
-				const markers = Array.from( modelDoc.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
+				const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
 				dispatcher.convertSelection( modelSelection, markers );
 
 				// Stringify view and check if it is same as expected.
@@ -344,7 +345,7 @@ describe( 'model-selection-to-view-converters', () => {
 
 			// #1072 - if the container has only ui elements, collapsed selection attribute should be rendered after those ui elements.
 			it( 'selection with attribute before ui element - no non-ui children', () => {
-				setModelData( modelDoc, '' );
+				setModelData( model, '' );
 
 				// Add two ui elements to view.
 				viewRoot.appendChildren( [
@@ -365,7 +366,7 @@ describe( 'model-selection-to-view-converters', () => {
 
 			// #1072.
 			it( 'selection with attribute before ui element - has non-ui children #1', () => {
-				setModelData( modelDoc, 'x' );
+				setModelData( model, 'x' );
 
 				modelSelection.setRanges( [ new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) ] );
 				modelSelection.setAttribute( 'bold', true );
@@ -386,7 +387,7 @@ describe( 'model-selection-to-view-converters', () => {
 
 			// #1072.
 			it( 'selection with attribute before ui element - has non-ui children #2', () => {
-				setModelData( modelDoc, '<$text bold="true">x</$text>y' );
+				setModelData( model, '<$text bold="true">x</$text>y' );
 
 				modelSelection.setRanges( [ new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) ] );
 				modelSelection.setAttribute( 'bold', true );
@@ -635,14 +636,14 @@ describe( 'model-selection-to-view-converters', () => {
 
 	describe( 'table cell selection converter', () => {
 		beforeEach( () => {
-			modelDoc.schema.registerItem( 'table' );
-			modelDoc.schema.registerItem( 'tr' );
-			modelDoc.schema.registerItem( 'td' );
+			model.schema.registerItem( 'table' );
+			model.schema.registerItem( 'tr' );
+			model.schema.registerItem( 'td' );
 
-			modelDoc.schema.allow( { name: 'table', inside: '$root' } );
-			modelDoc.schema.allow( { name: 'tr', inside: 'table' } );
-			modelDoc.schema.allow( { name: 'td', inside: 'tr' } );
-			modelDoc.schema.allow( { name: '$text', inside: 'td' } );
+			model.schema.allow( { name: 'table', inside: '$root' } );
+			model.schema.allow( { name: 'tr', inside: 'table' } );
+			model.schema.allow( { name: 'td', inside: 'tr' } );
+			model.schema.allow( { name: '$text', inside: 'td' } );
 
 			// "Universal" converter to convert table structure.
 			const tableConverter = insertElement( data => new ViewContainerElement( data.item.name ) );
@@ -705,7 +706,7 @@ describe( 'model-selection-to-view-converters', () => {
 	// that are offsets or paths of selection positions in root element.
 	function test( selectionPaths, modelInput, expectedView, selectionAttributes = {} ) {
 		// Parse passed `modelInput` string and set it as current model.
-		setModelData( modelDoc, modelInput );
+		setModelData( model, modelInput );
 
 		// Manually set selection ranges using passed `selectionPaths`.
 		const startPath = typeof selectionPaths[ 0 ] == 'number' ? [ selectionPaths[ 0 ] ] : selectionPaths[ 0 ];

+ 30 - 26
packages/ckeditor5-engine/tests/conversion/model-to-view-converters.js

@@ -3,7 +3,9 @@
  * For licensing, see LICENSE.md.
  */
 
-import ModelDocument from '../../src/model/document';
+import ModelWriter from '../../src/model/writer';
+import Batch from '../../src/model/batch';
+import Model from '../../src/model/model';
 import ModelElement from '../../src/model/element';
 import ModelText from '../../src/model/text';
 import ModelRange from '../../src/model/range';
@@ -35,19 +37,21 @@ import {
 import { createRangeOnElementOnly } from '../../tests/model/_utils/utils';
 
 describe( 'model-to-view-converters', () => {
-	let dispatcher, modelDoc, modelRoot, mapper, viewRoot, batch;
+	let dispatcher, model, modelDoc, modelRoot, modelWriter, mapper, viewRoot;
 
 	beforeEach( () => {
-		modelDoc = new ModelDocument();
+		model = new Model();
+		modelDoc = model.document;
 		modelRoot = modelDoc.createRoot();
 		viewRoot = new ViewContainerElement( 'div' );
 
-		batch = modelDoc.batch();
-
 		mapper = new Mapper();
 		mapper.bindElements( modelRoot, viewRoot );
 
-		dispatcher = new ModelConversionDispatcher( modelDoc, { mapper } );
+		dispatcher = new ModelConversionDispatcher( model, { mapper } );
+
+		// As an util for modifying model tree.
+		modelWriter = new ModelWriter( model, new Batch() );
 	} );
 
 	function viewAttributesToString( item ) {
@@ -110,7 +114,7 @@ describe( 'model-to-view-converters', () => {
 			dispatcher.on( 'removeMarker:marker', highlightText( highlightDescriptor ) );
 			dispatcher.convertInsertion( markerRange );
 
-			modelDoc.markers.set( 'marker', markerRange );
+			model.markers.set( 'marker', markerRange );
 			dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
 
 			expect( viewToString( viewRoot ) ).to.equal(
@@ -138,7 +142,7 @@ describe( 'model-to-view-converters', () => {
 			dispatcher.on( 'removeMarker:marker', highlightText( newDescriptor ), { priority: 'high' } );
 			dispatcher.convertInsertion( markerRange );
 
-			modelDoc.markers.set( 'marker', markerRange );
+			model.markers.set( 'marker', markerRange );
 			dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
 
 			expect( viewToString( viewRoot ) ).to.equal(
@@ -163,7 +167,7 @@ describe( 'model-to-view-converters', () => {
 
 			dispatcher.convertInsertion( markerRange );
 
-			modelDoc.markers.set( 'marker', markerRange );
+			model.markers.set( 'marker', markerRange );
 			dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
@@ -222,7 +226,7 @@ describe( 'model-to-view-converters', () => {
 			} ), { priority: 'high' } );
 
 			dispatcher.convertInsertion( markerRange );
-			modelDoc.markers.set( 'marker', markerRange );
+			model.markers.set( 'marker', markerRange );
 			dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
 
 			expect( viewToString( viewRoot ) ).to.equal(
@@ -264,7 +268,7 @@ describe( 'model-to-view-converters', () => {
 			} ), { priority: 'high' } );
 
 			dispatcher.convertInsertion( markerRange );
-			modelDoc.markers.set( 'marker', markerRange );
+			model.markers.set( 'marker', markerRange );
 			dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
 
 			expect( viewToString( viewRoot ) ).to.equal(
@@ -310,7 +314,7 @@ describe( 'model-to-view-converters', () => {
 			} ), { priority: 'high' } );
 
 			dispatcher.convertInsertion( markerRange );
-			modelDoc.markers.set( 'marker', markerRange );
+			model.markers.set( 'marker', markerRange );
 			dispatcher.convertMarker( 'addMarker', 'marker:foo-bar-baz', markerRange );
 		} );
 
@@ -320,7 +324,7 @@ describe( 'model-to-view-converters', () => {
 
 			dispatcher.convertInsertion( markerRange );
 
-			modelDoc.markers.set( 'marker', markerRange );
+			model.markers.set( 'marker', markerRange );
 			dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
@@ -538,7 +542,7 @@ describe( 'model-to-view-converters', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
 
-			batch.removeAttribute( 'bold', modelElement );
+			modelWriter.removeAttribute( 'bold', modelElement );
 
 			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'bold', true, null );
 
@@ -565,7 +569,7 @@ describe( 'model-to-view-converters', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
 
-			batch.removeAttribute( 'style', modelElement );
+			modelWriter.removeAttribute( 'style', modelElement );
 
 			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'style', 'bold', null );
 
@@ -595,7 +599,7 @@ describe( 'model-to-view-converters', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>x<a href="http://foo.com">foo</a>x</p></div>' );
 
-			batch.setAttribute( 'link', 'http://foobar.com', modelElement );
+			modelWriter.setAttribute( 'link', 'http://foobar.com', modelElement );
 
 			dispatcher.convertAttribute(
 				'changeAttribute',
@@ -623,7 +627,7 @@ describe( 'model-to-view-converters', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>நி<b>லைக்</b>கு</p></div>' );
 
-			batch.removeAttribute( 'bold', modelElement );
+			modelWriter.removeAttribute( 'bold', modelElement );
 
 			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'bold', true, null );
 
@@ -666,7 +670,7 @@ describe( 'model-to-view-converters', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
 
-			batch.removeAttribute( 'bold', modelElement );
+			modelWriter.removeAttribute( 'bold', modelElement );
 
 			dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'bold', true, null );
 
@@ -1012,7 +1016,7 @@ describe( 'model-to-view-converters', () => {
 
 			dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 
-			batch.remove( ModelRange.createFromParentsAndOffsets( modelDiv, 0, modelDiv, 6 ) );
+			modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelDiv, 0, modelDiv, 6 ) );
 
 			dispatcher.convertRemove(
 				ModelPosition.createFromParentAndOffset( modelDiv, 0 ),
@@ -1033,7 +1037,7 @@ describe( 'model-to-view-converters', () => {
 			dispatcher.on( 'remove', remove() );
 
 			// Remove 'b'.
-			batch.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 3, modelRoot, 4 ) );
+			modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 3, modelRoot, 4 ) );
 
 			dispatcher.convertRemove(
 				ModelPosition.createFromParentAndOffset( modelRoot, 3 ),
@@ -1054,7 +1058,7 @@ describe( 'model-to-view-converters', () => {
 			dispatcher.on( 'remove', remove() );
 
 			// Remove 'o<span></span>b'.
-			batch.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 2, modelRoot, 4 ) );
+			modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 2, modelRoot, 4 ) );
 
 			dispatcher.convertRemove(
 				ModelPosition.createFromParentAndOffset( modelRoot, 2 ),
@@ -1076,7 +1080,7 @@ describe( 'model-to-view-converters', () => {
 			dispatcher.on( 'remove', remove() );
 
 			// Move <a></a> after "b". Can be e.g. a part of an unwrap delta (move + remove).
-			batch.move(
+			modelWriter.move(
 				ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 ),
 				ModelPosition.createAt( modelRoot, 'end' )
 			);
@@ -1104,7 +1108,7 @@ describe( 'model-to-view-converters', () => {
 			dispatcher.on( 'remove', remove() );
 
 			// Remove <a></a>.
-			batch.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 ) );
+			modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 ) );
 
 			dispatcher.convertRemove(
 				ModelPosition.createFromParentAndOffset( modelRoot, 0 ),
@@ -1145,7 +1149,7 @@ describe( 'model-to-view-converters', () => {
 				dispatcher.convertChange( type, changes );
 			} );
 
-			modelDoc.batch().unwrap( modelWElement );
+			modelWriter.unwrap( modelWElement );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><a2></a2></div>' );
 
@@ -1177,7 +1181,7 @@ describe( 'model-to-view-converters', () => {
 
 			dispatcher.on( 'remove', remove() );
 
-			batch.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+			modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
 
 			dispatcher.convertRemove(
 				ModelPosition.createFromParentAndOffset( modelRoot, 1 ),
@@ -1201,7 +1205,7 @@ describe( 'model-to-view-converters', () => {
 
 			dispatcher.on( 'remove', remove() );
 
-			batch.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 3, modelRoot, 4 ) );
+			modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 3, modelRoot, 4 ) );
 
 			dispatcher.convertRemove(
 				ModelPosition.createFromParentAndOffset( modelRoot, 3 ),

+ 63 - 53
packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js

@@ -4,11 +4,13 @@
  */
 
 import ModelConversionDispatcher from '../../src/conversion/modelconversiondispatcher';
-import ModelDocument from '../../src/model/document';
+import Model from '../../src/model/model';
 import ModelText from '../../src/model/text';
 import ModelElement from '../../src/model/element';
 import ModelRange from '../../src/model/range';
 import ModelPosition from '../../src/model/position';
+import ModelWriter from '../../src/model/writer';
+import Batch from '../../src/model/batch';
 import RemoveOperation from '../../src/model/operation/removeoperation';
 import NoOperation from '../../src/model/operation/nooperation';
 import RenameOperation from '../../src/model/operation/renameoperation';
@@ -18,20 +20,24 @@ import { wrapInDelta } from '../../tests/model/_utils/utils';
 import ViewContainerElement from '../../src/view/containerelement';
 
 describe( 'ModelConversionDispatcher', () => {
-	let dispatcher, doc, root, gyPos;
+	let dispatcher, doc, root, gyPos, model, writer;
 
 	beforeEach( () => {
-		doc = new ModelDocument();
-		dispatcher = new ModelConversionDispatcher( doc );
+		model = new Model();
+		doc = model.document;
+		dispatcher = new ModelConversionDispatcher( model );
 		root = doc.createRoot();
 
 		gyPos = new ModelPosition( doc.graveyard, [ 0 ] );
+
+		// As an util for modifying model tree.
+		writer = new ModelWriter( model, new Batch() );
 	} );
 
 	describe( 'constructor()', () => {
 		it( 'should create ModelConversionDispatcher with given api', () => {
 			const apiObj = {};
-			const dispatcher = new ModelConversionDispatcher( doc, { apiObj } );
+			const dispatcher = new ModelConversionDispatcher( model, { apiObj } );
 
 			expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
 		} );
@@ -63,7 +69,9 @@ describe( 'ModelConversionDispatcher', () => {
 			dispatcher.on( 'insert:image', cbInsertImage );
 			dispatcher.on( 'addAttribute:key:$text', cbAddAttribute );
 
-			doc.batch().insertText( 'foo', { key: 'value' }, root );
+			model.change( writer => {
+				writer.insertText( 'foo', { key: 'value' }, root );
+			} );
 
 			expect( cbInsertText.called ).to.be.true;
 			expect( cbAddAttribute.called ).to.be.true;
@@ -78,7 +86,7 @@ describe( 'ModelConversionDispatcher', () => {
 			const removeOperation = new RemoveOperation( imagePos, 1, gyPos, 0 );
 
 			// Let's apply remove operation so reinsert operation won't break.
-			doc.applyOperation( wrapInDelta( removeOperation ) );
+			model.applyOperation( wrapInDelta( removeOperation ) );
 
 			const cbInsertText = sinon.spy();
 			const cbInsertImage = sinon.spy();
@@ -88,7 +96,7 @@ describe( 'ModelConversionDispatcher', () => {
 			dispatcher.on( 'insert:image', cbInsertImage );
 			dispatcher.on( 'addAttribute:key:image', cbAddAttribute );
 
-			doc.applyOperation( wrapInDelta( removeOperation.getReversed() ) );
+			model.applyOperation( wrapInDelta( removeOperation.getReversed() ) );
 
 			expect( cbInsertImage.called ).to.be.true;
 			expect( cbAddAttribute.called ).to.be.true;
@@ -100,7 +108,9 @@ describe( 'ModelConversionDispatcher', () => {
 
 			dispatcher.on( 'remove', cbRemove );
 
-			doc.batch().remove( image );
+			model.change( writer => {
+				writer.remove( image );
+			} );
 
 			expect( cbRemove.called ).to.be.true;
 		} );
@@ -112,13 +122,17 @@ describe( 'ModelConversionDispatcher', () => {
 			dispatcher.on( 'addAttribute:key:$text', cbAddText );
 			dispatcher.on( 'addAttribute:key:image', cbAddImage );
 
-			doc.batch().setAttribute( 'key', 'value', image );
+			model.change( writer => {
+				writer.setAttribute( 'key', 'value', image );
+			} );
 
 			// Callback for adding attribute on text not called.
 			expect( cbAddText.called ).to.be.false;
 			expect( cbAddImage.calledOnce ).to.be.true;
 
-			doc.batch().setAttribute( 'key', 'value', ModelRange.createFromParentsAndOffsets( root, 3, root, 4 ) );
+			model.change( writer => {
+				writer.setAttribute( 'key', 'value', ModelRange.createFromParentsAndOffsets( root, 3, root, 4 ) );
+			} );
 
 			expect( cbAddText.calledOnce ).to.be.true;
 			// Callback for adding attribute on image not called this time.
@@ -128,21 +142,24 @@ describe( 'ModelConversionDispatcher', () => {
 		it( 'should fire changeAttribute callbacks for change attribute change', () => {
 			const cbChangeText = sinon.spy();
 			const cbChangeImage = sinon.spy();
-			const batch = doc.batch();
 
 			dispatcher.on( 'changeAttribute:key:$text', cbChangeText );
 			dispatcher.on( 'changeAttribute:key:image', cbChangeImage );
 
-			batch.setAttribute( 'key', 'value', image );
-			batch.setAttribute( 'key', 'newValue', image );
+			model.change( writer => {
+				writer.setAttribute( 'key', 'value', image );
+				writer.setAttribute( 'key', 'newValue', image );
+			} );
 
 			// Callback for adding attribute on text not called.
 			expect( cbChangeText.called ).to.be.false;
 			expect( cbChangeImage.calledOnce ).to.be.true;
 
 			const range = ModelRange.createFromParentsAndOffsets( root, 3, root, 4 );
-			batch.setAttribute( 'key', 'value', range );
-			batch.setAttribute( 'key', 'newValue', range );
+			model.change( writer => {
+				writer.setAttribute( 'key', 'value', range );
+				writer.setAttribute( 'key', 'newValue', range );
+			} );
 
 			expect( cbChangeText.calledOnce ).to.be.true;
 			// Callback for adding attribute on image not called this time.
@@ -152,21 +169,20 @@ describe( 'ModelConversionDispatcher', () => {
 		it( 'should fire removeAttribute callbacks for remove attribute change', () => {
 			const cbRemoveText = sinon.spy();
 			const cbRemoveImage = sinon.spy();
-			const batch = doc.batch();
 
 			dispatcher.on( 'removeAttribute:key:$text', cbRemoveText );
 			dispatcher.on( 'removeAttribute:key:image', cbRemoveImage );
 
-			batch.setAttribute( 'key', 'value', image );
-			batch.removeAttribute( 'key', image );
+			writer.setAttribute( 'key', 'value', image );
+			writer.removeAttribute( 'key', image );
 
 			// Callback for adding attribute on text not called.
 			expect( cbRemoveText.called ).to.be.false;
 			expect( cbRemoveImage.calledOnce ).to.be.true;
 
 			const range = ModelRange.createFromParentsAndOffsets( root, 3, root, 4 );
-			batch.setAttribute( 'key', 'value', range );
-			batch.removeAttribute( 'key', range );
+			writer.setAttribute( 'key', 'value', range );
+			writer.removeAttribute( 'key', range );
 
 			expect( cbRemoveText.calledOnce ).to.be.true;
 			// Callback for adding attribute on image not called this time.
@@ -187,7 +203,7 @@ describe( 'ModelConversionDispatcher', () => {
 			const gyNode = new ModelElement( 'image' );
 			doc.graveyard.appendChildren( gyNode );
 
-			doc.batch().setAttribute( 'key', 'value', gyNode );
+			writer.setAttribute( 'key', 'value', gyNode );
 
 			expect( dispatcher.fire.called ).to.be.false;
 		} );
@@ -199,7 +215,7 @@ describe( 'ModelConversionDispatcher', () => {
 			const gyNode = new ModelElement( 'image' );
 			doc.graveyard.appendChildren( gyNode );
 
-			doc.batch().remove( gyNode );
+			writer.remove( gyNode );
 
 			expect( dispatcher.fire.called ).to.be.false;
 		} );
@@ -211,7 +227,7 @@ describe( 'ModelConversionDispatcher', () => {
 			const gyNode = new ModelElement( 'image' );
 			doc.graveyard.appendChildren( gyNode );
 
-			doc.batch().rename( gyNode, 'p' );
+			writer.rename( gyNode, 'p' );
 
 			expect( dispatcher.fire.called ).to.be.false;
 		} );
@@ -219,7 +235,7 @@ describe( 'ModelConversionDispatcher', () => {
 		it( 'should not fire any event after NoOperation is applied', () => {
 			sinon.spy( dispatcher, 'fire' );
 
-			doc.applyOperation( wrapInDelta( new NoOperation( 0 ) ) );
+			model.applyOperation( wrapInDelta( new NoOperation( 0 ) ) );
 
 			expect( dispatcher.fire.called ).to.be.false;
 		} );
@@ -230,7 +246,7 @@ describe( 'ModelConversionDispatcher', () => {
 			root.removeChildren( 0, root.childCount );
 			root.appendChildren( [ new ModelElement( 'paragraph' ) ] );
 
-			doc.applyOperation( wrapInDelta( new RenameOperation( new ModelPosition( root, [ 0 ] ), 'paragraph', 'paragraph', 0 ) ) );
+			model.applyOperation( wrapInDelta( new RenameOperation( new ModelPosition( root, [ 0 ] ), 'paragraph', 'paragraph', 0 ) ) );
 
 			expect( dispatcher.fire.called ).to.be.false;
 		} );
@@ -242,7 +258,7 @@ describe( 'ModelConversionDispatcher', () => {
 			root.appendChildren( [ new ModelElement( 'paragraph', { foo: 'bar' } ) ] );
 
 			const range = new ModelRange( new ModelPosition( root, [ 0 ] ), new ModelPosition( root, [ 0, 0 ] ) );
-			doc.applyOperation( wrapInDelta( new AttributeOperation( range, 'foo', 'bar', 'bar', 0 ) ) );
+			model.applyOperation( wrapInDelta( new AttributeOperation( range, 'foo', 'bar', 'bar', 0 ) ) );
 
 			expect( dispatcher.fire.called ).to.be.false;
 		} );
@@ -337,7 +353,7 @@ describe( 'ModelConversionDispatcher', () => {
 			root.appendChildren( [ paragraph1, paragraph2 ] );
 
 			const markerRange = ModelRange.createFromParentsAndOffsets( root, 0, root, 2 );
-			doc.markers.set( 'marker', markerRange );
+			model.markers.set( 'marker', markerRange );
 
 			const insertionRange = ModelRange.createOn( paragraph2 );
 			dispatcher.convertInsertion( insertionRange );
@@ -356,7 +372,7 @@ describe( 'ModelConversionDispatcher', () => {
 			root.appendChildren( [ paragraph1, paragraph2 ] );
 
 			const markerRange = ModelRange.createIn( paragraph2 );
-			doc.markers.set( 'marker', markerRange );
+			model.markers.set( 'marker', markerRange );
 
 			const insertionRange = ModelRange.createOn( paragraph2 );
 			dispatcher.convertInsertion( insertionRange );
@@ -396,7 +412,7 @@ describe( 'ModelConversionDispatcher', () => {
 			};
 
 			const markerRange = ModelRange.createFromParentsAndOffsets( root, 0, root, 1 );
-			doc.markers.set( 'marker', markerRange );
+			model.markers.set( 'marker', markerRange );
 
 			const insertionRange = ModelRange.createFromParentsAndOffsets( caption, 1, caption, 2 );
 			dispatcher.convertInsertion( insertionRange );
@@ -432,7 +448,7 @@ describe( 'ModelConversionDispatcher', () => {
 			};
 
 			const markerRange = ModelRange.createFromParentsAndOffsets( caption, 0, caption, 3 );
-			doc.markers.set( 'marker', markerRange );
+			model.markers.set( 'marker', markerRange );
 
 			const insertionRange = ModelRange.createFromParentsAndOffsets( caption, 2, caption, 3 );
 			dispatcher.convertInsertion( insertionRange );
@@ -620,11 +636,9 @@ describe( 'ModelConversionDispatcher', () => {
 		} );
 
 		it( 'should prepare correct list of consumable values', () => {
-			doc.enqueueChanges( () => {
-				const batch = doc.batch();
-
-				batch.setAttribute( 'bold', true, ModelRange.createIn( root ) );
-				batch.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
+			model.enqueueChange( writer => {
+				writer.setAttribute( 'bold', true, ModelRange.createIn( root ) );
+				writer.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
 			} );
 
 			dispatcher.on( 'selection', ( evt, data, consumable ) => {
@@ -639,11 +653,9 @@ describe( 'ModelConversionDispatcher', () => {
 		it( 'should fire attributes events for selection', () => {
 			sinon.spy( dispatcher, 'fire' );
 
-			doc.enqueueChanges( () => {
-				const batch = doc.batch();
-
-				batch.setAttribute( 'bold', true, ModelRange.createIn( root ) );
-				batch.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
+			model.enqueueChange( writer => {
+				writer.setAttribute( 'bold', true, ModelRange.createIn( root ) );
+				writer.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
 			} );
 
 			dispatcher.convertSelection( doc.selection, [] );
@@ -659,11 +671,9 @@ describe( 'ModelConversionDispatcher', () => {
 				consumable.consume( data.selection, 'selectionAttribute:bold' );
 			} );
 
-			doc.enqueueChanges( () => {
-				const batch = doc.batch();
-
-				batch.setAttribute( 'bold', true, ModelRange.createIn( root ) );
-				batch.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
+			model.enqueueChange( writer => {
+				writer.setAttribute( 'bold', true, ModelRange.createIn( root ) );
+				writer.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
 			} );
 
 			dispatcher.convertSelection( doc.selection, [] );
@@ -672,11 +682,11 @@ describe( 'ModelConversionDispatcher', () => {
 		} );
 
 		it( 'should fire events for each marker which contains selection', () => {
-			doc.markers.set( 'name', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
+			model.markers.set( 'name', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
 
 			sinon.spy( dispatcher, 'fire' );
 
-			const markers = Array.from( doc.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
+			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
 			dispatcher.convertSelection( doc.selection, markers );
 
 			expect( dispatcher.fire.calledWith( 'selectionMarker:name' ) ).to.be.true;
@@ -710,12 +720,12 @@ describe( 'ModelConversionDispatcher', () => {
 				}
 			};
 
-			doc.markers.set( 'name', ModelRange.createFromParentsAndOffsets( root, 0, root, 1 ) );
+			model.markers.set( 'name', ModelRange.createFromParentsAndOffsets( root, 0, root, 1 ) );
 			doc.selection.setRanges( [ ModelRange.createFromParentsAndOffsets( caption, 1, caption, 1 ) ] );
 
 			sinon.spy( dispatcher, 'fire' );
 
-			const markers = Array.from( doc.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
+			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
 
 			dispatcher.convertSelection( doc.selection, markers );
 
@@ -723,8 +733,8 @@ describe( 'ModelConversionDispatcher', () => {
 		} );
 
 		it( 'should not fire events if information about marker has been consumed', () => {
-			doc.markers.set( 'foo', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
-			doc.markers.set( 'bar', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
+			model.markers.set( 'foo', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
+			model.markers.set( 'bar', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
 
 			sinon.spy( dispatcher, 'fire' );
 
@@ -732,7 +742,7 @@ describe( 'ModelConversionDispatcher', () => {
 				consumable.consume( data.selection, 'selectionMarker:bar' );
 			} );
 
-			const markers = Array.from( doc.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
+			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
 			dispatcher.convertSelection( doc.selection, markers );
 
 			expect( dispatcher.fire.calledWith( 'selectionMarker:foo' ) ).to.be.true;

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

@@ -7,7 +7,7 @@ import ViewDocument from '../../src/view/document';
 import ViewSelection from '../../src/view/selection';
 import ViewRange from '../../src/view/range';
 
-import ModelDocument from '../../src/model/document';
+import Model from '../../src/model/model';
 
 import Mapper from '../../src/conversion/mapper';
 import { convertSelectionChange } from '../../src/conversion/view-selection-to-model-converters';
@@ -19,8 +19,8 @@ describe( 'convertSelectionChange', () => {
 	let model, view, mapper, convertSelection, modelRoot, viewRoot;
 
 	beforeEach( () => {
-		model = new ModelDocument();
-		modelRoot = model.createRoot();
+		model = new Model();
+		modelRoot = model.document.createRoot();
 		model.schema.registerItem( 'paragraph', '$block' );
 
 		modelSetData( model, '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
@@ -82,7 +82,7 @@ describe( 'convertSelectionChange', () => {
 		expect( modelGetData( model ) ).to.equal(
 			'<paragraph>f[o]o</paragraph><paragraph>b[a]r</paragraph>' );
 
-		const ranges = Array.from( model.selection.getRanges() );
+		const ranges = Array.from( model.document.selection.getRanges() );
 		expect( ranges.length ).to.equal( 2 );
 
 		expect( ranges[ 0 ].start.parent ).to.equal( modelRoot.getChild( 0 ) );
@@ -106,7 +106,7 @@ describe( 'convertSelectionChange', () => {
 		convertSelection( null, { newSelection: viewSelection } );
 
 		expect( modelGetData( model ) ).to.equal( '<paragraph>f[o]o</paragraph><paragraph>b[a]r</paragraph>' );
-		expect( model.selection.isBackward ).to.true;
+		expect( model.document.selection.isBackward ).to.true;
 	} );
 
 	it( 'should not enqueue changes if selection has not changed', () => {

+ 11 - 12
packages/ckeditor5-engine/tests/conversion/view-to-model-converters.js

@@ -8,8 +8,8 @@ import ViewContainerElement from '../../src/view/containerelement';
 import ViewDocumentFragment from '../../src/view/documentfragment';
 import ViewText from '../../src/view/text';
 
+import Model from '../../src/model/model';
 import 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';
@@ -17,17 +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, additionalData, batch;
+	let dispatcher, schema, additionalData;
 
-	const modelDocument = new ModelDocument();
+	const model = new Model();
 
 	beforeEach( () => {
 		schema = new ModelSchema();
 		schema.registerItem( 'paragraph', '$block' );
 		schema.allow( { name: '$text', inside: '$root' } );
-		batch = modelDocument.batch();
 		additionalData = { context: [ '$root' ] };
-		dispatcher = new ViewConversionDispatcher( { schema } );
+		dispatcher = new ViewConversionDispatcher( model, { schema } );
 	} );
 
 	describe( 'convertText', () => {
@@ -36,7 +35,7 @@ describe( 'view-to-model-converters', () => {
 
 			dispatcher.on( 'text', convertText() );
 
-			const conversionResult = dispatcher.convert( viewText, batch, additionalData );
+			const conversionResult = dispatcher.convert( viewText, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -55,7 +54,7 @@ describe( 'view-to-model-converters', () => {
 				}
 			} );
 
-			const conversionResult = dispatcher.convert( viewText, batch, additionalData );
+			const conversionResult = dispatcher.convert( viewText, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -68,12 +67,12 @@ describe( 'view-to-model-converters', () => {
 			const viewText = new ViewText( 'foobar' );
 			dispatcher.on( 'text', convertText() );
 
-			let conversionResult = dispatcher.convert( viewText, batch, additionalData );
+			let conversionResult = dispatcher.convert( viewText, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 0 );
 
-			conversionResult = dispatcher.convert( viewText, batch, { context: [ '$block' ] } );
+			conversionResult = dispatcher.convert( viewText, { context: [ '$block' ] } );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 1 );
@@ -86,7 +85,7 @@ describe( 'view-to-model-converters', () => {
 
 			dispatcher.on( 'text', convertText() );
 
-			const conversionResult = dispatcher.convert( viewText, batch, additionalData );
+			const conversionResult = dispatcher.convert( viewText, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -107,7 +106,7 @@ describe( 'view-to-model-converters', () => {
 			dispatcher.on( 'element', convertToModelFragment() );
 			dispatcher.on( 'documentFragment', convertToModelFragment() );
 
-			const conversionResult = dispatcher.convert( viewFragment, batch, additionalData );
+			const conversionResult = dispatcher.convert( viewFragment, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.maxOffset ).to.equal( 6 );
@@ -132,7 +131,7 @@ describe( 'view-to-model-converters', () => {
 				}
 			} );
 
-			const conversionResult = dispatcher.convert( viewP, batch, additionalData );
+			const conversionResult = dispatcher.convert( viewP, additionalData );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement );

+ 23 - 27
packages/ckeditor5-engine/tests/conversion/viewconversiondispatcher.js

@@ -8,10 +8,10 @@ import ViewContainerElement from '../../src/view/containerelement';
 import ViewDocumentFragment from '../../src/view/documentfragment';
 import ViewText from '../../src/view/text';
 
+import Model from '../../src/model/model';
 import 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';
@@ -20,15 +20,19 @@ import log from '@ckeditor/ckeditor5-utils/src/log';
 const logWarn = log.warn;
 
 describe( 'ViewConversionDispatcher', () => {
+	let model;
+
 	afterEach( () => {
+		model = new Model();
 		log.warn = logWarn;
 	} );
 
 	describe( 'constructor()', () => {
 		it( 'should create ViewConversionDispatcher with passed api', () => {
 			const apiObj = {};
-			const dispatcher = new ViewConversionDispatcher( { 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 );
@@ -36,13 +40,10 @@ describe( 'ViewConversionDispatcher', () => {
 	} );
 
 	describe( 'convert', () => {
-		let dispatcher, batch;
-
-		const modelDocument = new ModelDocument();
+		let dispatcher;
 
 		beforeEach( () => {
-			dispatcher = new ViewConversionDispatcher();
-			batch = modelDocument.batch();
+			dispatcher = new ViewConversionDispatcher( model );
 		} );
 
 		it( 'should fire viewCleanup event on converted view part', () => {
@@ -51,7 +52,7 @@ describe( 'ViewConversionDispatcher', () => {
 			sinon.spy( dispatcher, 'fire' );
 
 			const viewP = new ViewContainerElement( 'p' );
-			dispatcher.convert( viewP, batch );
+			dispatcher.convert( viewP );
 
 			expect( dispatcher.fire.calledWith( 'viewCleanup', viewP ) ).to.be.true;
 		} );
@@ -65,9 +66,9 @@ describe( 'ViewConversionDispatcher', () => {
 
 			sinon.spy( dispatcher, 'fire' );
 
-			dispatcher.convert( viewText, batch );
-			dispatcher.convert( viewElement, batch );
-			dispatcher.convert( viewFragment, batch );
+			dispatcher.convert( viewText );
+			dispatcher.convert( viewElement );
+			dispatcher.convert( viewFragment );
 
 			expect( dispatcher.fire.calledWith( 'text' ) ).to.be.true;
 			expect( dispatcher.fire.calledWith( 'element:p' ) ).to.be.true;
@@ -99,7 +100,7 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewText, batch, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewText, { foo: 'bar' } );
 
 			// Check conversion result.
 			// Result should be wrapped in document fragment.
@@ -134,7 +135,7 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewElement, batch, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewElement, { foo: 'bar' } );
 
 			// Check conversion result.
 			// Result should be wrapped in document fragment.
@@ -168,7 +169,7 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewFragment, batch, { foo: 'bar' } );
+			const conversionResult = dispatcher.convert( viewFragment, { foo: 'bar' } );
 
 			// Check conversion result.
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
@@ -191,7 +192,7 @@ describe( 'ViewConversionDispatcher', () => {
 				] );
 			} );
 
-			const conversionResult = dispatcher.convert( viewFragment, batch );
+			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' );
@@ -201,13 +202,9 @@ describe( 'ViewConversionDispatcher', () => {
 
 	describe( 'conversionApi', () => {
 		let spy, spyP, spyText, viewP, viewText, modelP, modelText, consumableMock, dispatcher,
-			spyNull, spyArray, viewDiv, viewNull, viewArray, batch;
-
-		const modelDocument = new ModelDocument();
+			spyNull, spyArray, viewDiv, viewNull, viewArray;
 
 		beforeEach( () => {
-			batch = modelDocument.batch();
-
 			spy = sinon.spy();
 			spyP = sinon.spy();
 			spyText = sinon.spy();
@@ -219,7 +216,7 @@ describe( 'ViewConversionDispatcher', () => {
 
 			consumableMock = {};
 
-			dispatcher = new ViewConversionDispatcher();
+			dispatcher = new ViewConversionDispatcher( model );
 
 			dispatcher.on( 'element:p', ( evt, data, consumable ) => {
 				spyP();
@@ -268,10 +265,9 @@ describe( 'ViewConversionDispatcher', () => {
 
 					expect( conversionApi.convertItem( viewP, consumableMock, data ) ).to.equal( modelP );
 					expect( conversionApi.convertItem( viewText, consumableMock, data ) ).to.equal( modelText );
-					expect( conversionApi.batch ).to.equal( batch );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment(), batch, { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment(), { foo: 'bar' } );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyP.calledOnce ).to.be.true;
@@ -288,7 +284,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( conversionApi.convertItem( viewNull ) ).to.equal( null );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment(), batch );
+				dispatcher.convert( new ViewDocumentFragment() );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyNull.calledOnce ).to.be.true;
@@ -306,7 +302,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( conversionApi.convertItem( viewArray ) ).to.equal( null );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment(), batch );
+				dispatcher.convert( new ViewDocumentFragment() );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyArray.calledOnce ).to.be.true;
@@ -332,7 +328,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( result.getChild( 1 ) ).to.equal( modelText );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), batch, { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), { foo: 'bar' } );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyP.calledOnce ).to.be.true;
@@ -353,7 +349,7 @@ describe( 'ViewConversionDispatcher', () => {
 					expect( result.getChild( 1 ) ).to.equal( modelText );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), batch, { foo: 'bar' } );
+				dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), { foo: 'bar' } );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyNull.calledOnce ).to.be.true;

+ 32 - 31
packages/ckeditor5-engine/tests/dev-utils/deltareplayer.js

@@ -4,23 +4,23 @@
  */
 
 import DeltaReplayer from '../../src/dev-utils/deltareplayer';
-import Document from '../../src/model/document';
+import Model from '../../src/model/model';
 
 describe( 'DeltaReplayer', () => {
 	describe( 'constructor()', () => {
 		it( 'should be able to initialize replayer without deltas', () => {
-			const doc = getDocument();
+			const model = getModel();
 			const stringifiedDeltas = '';
-			const deltaReplayer = new DeltaReplayer( doc, '---', stringifiedDeltas );
+			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
 
 			expect( deltaReplayer.getDeltasToReplay() ).to.deep.equal( [] );
 		} );
 
 		it( 'should be able to initialize replayer with deltas', () => {
-			const doc = getDocument();
+			const model = getModel();
 			const delta = getFirstDelta();
 
-			const deltaReplayer = new DeltaReplayer( doc, '---', JSON.stringify( delta ) );
+			const deltaReplayer = new DeltaReplayer( model, '---', JSON.stringify( delta ) );
 
 			expect( deltaReplayer.getDeltasToReplay() ).to.deep.equal( [ delta ] );
 		} );
@@ -28,10 +28,10 @@ describe( 'DeltaReplayer', () => {
 
 	describe( 'applyNextDelta()', () => {
 		it( 'should remove first delta from stack', () => {
-			const doc = getDocument();
+			const model = getModel();
 			const delta = getFirstDelta();
 
-			const deltaReplayer = new DeltaReplayer( doc, '---', JSON.stringify( delta ) );
+			const deltaReplayer = new DeltaReplayer( model, '---', JSON.stringify( delta ) );
 
 			return deltaReplayer.applyNextDelta().then( isFinished => {
 				expect( deltaReplayer.getDeltasToReplay() ).to.deep.equal( [] );
@@ -40,19 +40,19 @@ describe( 'DeltaReplayer', () => {
 		} );
 
 		it( 'should apply first delta on the document', () => {
-			const doc = getDocument();
+			const model = getModel();
 			const delta = getFirstDelta();
 
-			const deltaReplayer = new DeltaReplayer( doc, '---', JSON.stringify( delta ) );
+			const deltaReplayer = new DeltaReplayer( model, '---', JSON.stringify( delta ) );
 
 			return deltaReplayer.applyNextDelta().then( () => {
-				expect( Array.from( doc.getRoot().getChildren() ).length ).to.equal( 1 );
+				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 1 );
 			} );
 		} );
 
 		it( 'should resolve with true if 0 deltas are provided', () => {
-			const doc = getDocument();
-			const deltaReplayer = new DeltaReplayer( doc, '---', '' );
+			const model = getModel();
+			const deltaReplayer = new DeltaReplayer( model, '---', '' );
 
 			return deltaReplayer.applyNextDelta().then( isFinished => {
 				expect( isFinished ).to.equal( true );
@@ -62,16 +62,16 @@ describe( 'DeltaReplayer', () => {
 
 	describe( 'applyAllDeltas()', () => {
 		it( 'should apply all deltas on the document', () => {
-			const doc = getDocument();
+			const model = getModel();
 
 			const stringifiedDeltas = [ getFirstDelta(), getSecondDelta() ]
 				.map( d => JSON.stringify( d ) )
 				.join( '---' );
 
-			const deltaReplayer = new DeltaReplayer( doc, '---', stringifiedDeltas );
+			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
 
 			return deltaReplayer.applyAllDeltas().then( () => {
-				expect( Array.from( doc.getRoot().getChildren() ).length ).to.equal( 2 );
+				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 2 );
 				expect( deltaReplayer.getDeltasToReplay().length ).to.equal( 0 );
 			} );
 		} );
@@ -79,31 +79,31 @@ describe( 'DeltaReplayer', () => {
 
 	describe( 'applyDeltas()', () => {
 		it( 'should apply certain number of deltas on the document', () => {
-			const doc = getDocument();
+			const model = getModel();
 
 			const stringifiedDeltas = [ getFirstDelta(), getSecondDelta() ]
 				.map( d => JSON.stringify( d ) )
 				.join( '---' );
 
-			const deltaReplayer = new DeltaReplayer( doc, '---', stringifiedDeltas );
+			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
 
 			return deltaReplayer.applyDeltas( 1 ).then( () => {
-				expect( Array.from( doc.getRoot().getChildren() ).length ).to.equal( 1 );
+				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 1 );
 				expect( deltaReplayer.getDeltasToReplay().length ).to.equal( 1 );
 			} );
 		} );
 
 		it( 'should not throw an error if the number of deltas is lower than number of expected deltas to replay', () => {
-			const doc = getDocument();
+			const model = getModel();
 
 			const stringifiedDeltas = [ getFirstDelta(), getSecondDelta() ]
 				.map( d => JSON.stringify( d ) )
 				.join( '---' );
 
-			const deltaReplayer = new DeltaReplayer( doc, '---', stringifiedDeltas );
+			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
 
 			return deltaReplayer.applyDeltas( 3 ).then( () => {
-				expect( Array.from( doc.getRoot().getChildren() ).length ).to.equal( 2 );
+				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 2 );
 				expect( deltaReplayer.getDeltasToReplay().length ).to.equal( 0 );
 			} );
 		} );
@@ -111,13 +111,13 @@ describe( 'DeltaReplayer', () => {
 
 	describe( 'play()', () => {
 		it( 'should play deltas with time interval', () => {
-			const doc = getDocument();
+			const model = getModel();
 
 			const stringifiedDeltas = [ getFirstDelta(), getSecondDelta() ]
 				.map( d => JSON.stringify( d ) )
 				.join( '---' );
 
-			const deltaReplayer = new DeltaReplayer( doc, '---', stringifiedDeltas );
+			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
 
 			return deltaReplayer.play( 0 ).then( () => {
 				expect( deltaReplayer.getDeltasToReplay().length ).to.equal( 0 );
@@ -125,15 +125,15 @@ describe( 'DeltaReplayer', () => {
 		} );
 
 		it( 'should work with default time interval', () => {
-			const doc = getDocument();
+			const model = getModel();
 
-			const deltaReplayer = new DeltaReplayer( doc, '---', '' );
+			const deltaReplayer = new DeltaReplayer( model, '---', '' );
 
 			return deltaReplayer.play();
 		} );
 
 		it( 'should correctly handle errors coming from the engine', () => {
-			const doc = getDocument();
+			const model = getModel();
 
 			const invalidDelta = getSecondDelta();
 			invalidDelta.operations[ 0 ].baseVersion = 3;
@@ -142,7 +142,7 @@ describe( 'DeltaReplayer', () => {
 				.map( d => JSON.stringify( d ) )
 				.join( '---' );
 
-			const deltaReplayer = new DeltaReplayer( doc, '---', stringifiedDeltas );
+			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
 
 			return deltaReplayer.play( 1 )
 				.then( () => {
@@ -154,11 +154,12 @@ describe( 'DeltaReplayer', () => {
 	} );
 } );
 
-function getDocument() {
-	const doc = new Document();
-	doc.createRoot( 'main' );
+function getModel() {
+	const model = new Model();
 
-	return doc;
+	model.document.createRoot();
+
+	return model;
 }
 
 function getFirstDelta() {

+ 65 - 50
packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

@@ -35,7 +35,7 @@ import SplitDelta from '../../src/model/delta/splitdelta';
 import UnwrapDelta from '../../src/model/delta/unwrapdelta';
 import WrapDelta from '../../src/model/delta/wrapdelta';
 import deltaTransform from '../../src/model/delta/transform';
-import ModelDocument from '../../src/model/document';
+import Model from '../../src/model/model';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 
 import ViewDocument from '../../src/view/document';
@@ -78,7 +78,7 @@ describe( 'disableEngineDebug', () => {
 		expect( ModelElement.prototype.printTree ).to.equal( undefined, 'Initial value (model/element)' );
 		expect( Delta.prototype.log ).to.equal( undefined, 'Initial value (model/delta/delta)' );
 		expect( ViewElement.prototype.printTree ).to.equal( undefined, 'Initial value (view/element)' );
-		expect( ModelDocument.prototype.createReplayer ).to.equal( undefined, 'Initial value (model/document)' );
+		expect( Model.prototype.createReplayer ).to.equal( undefined, 'Initial value (model/document)' );
 		expect( Editor.prototype.logDocuments ).to.equal( undefined, 'Initial value (core~editor/editor)' );
 
 		enableEngineDebug();
@@ -87,7 +87,7 @@ describe( 'disableEngineDebug', () => {
 		expect( ModelElement.prototype.printTree ).to.be.a( 'function', 'After enabling engine debug (model/element)' );
 		expect( Delta.prototype.log ).to.be.a( 'function', 'After enabling engine debug (model/delta/delta)' );
 		expect( ViewElement.prototype.printTree ).to.be.a( 'function', 'After enabling engine debug (view/element)' );
-		expect( ModelDocument.prototype.createReplayer ).to.be.a( 'function', 'After enabling engine debug (model/document)' );
+		expect( Model.prototype.createReplayer ).to.be.a( 'function', 'After enabling engine debug (model/document)' );
 		expect( Editor.prototype.logDocuments ).to.be.a( 'function', 'After enabling engine debug (core~editor/editor)' );
 
 		disableEngineDebug();
@@ -96,7 +96,7 @@ describe( 'disableEngineDebug', () => {
 		expect( ModelElement.prototype.printTree ).to.equal( undefined, 'After disabling engine debug (model/element)' );
 		expect( Delta.prototype.log ).to.equal( undefined, 'After disabling engine debug (model/delta/delta)' );
 		expect( ViewElement.prototype.printTree ).to.equal( undefined, 'After disabling engine debug (view/element)' );
-		expect( ModelDocument.prototype.createReplayer ).to.equal( undefined, 'After disabling engine debug (model/document)' );
+		expect( Model.prototype.createReplayer ).to.equal( undefined, 'After disabling engine debug (model/document)' );
 		expect( Editor.prototype.logDocuments ).to.equal( undefined, 'After disabling engine debug (core~editor/editor)' );
 	} );
 } );
@@ -108,7 +108,7 @@ describe( 'debug tools', () => {
 		constructor( ...args ) {
 			super( ...args );
 
-			this.document.createRoot( 'main' );
+			this.model.document.createRoot( 'main' );
 			this.editing.createRoot( this.element, 'main' );
 		}
 	}
@@ -128,10 +128,11 @@ describe( 'debug tools', () => {
 	} );
 
 	describe( 'should provide logging tools', () => {
-		let modelDoc, modelRoot, modelElement, modelDocFrag;
+		let model, modelDoc, modelRoot, modelElement, modelDocFrag;
 
 		beforeEach( () => {
-			modelDoc = new ModelDocument();
+			model = new Model();
+			modelDoc = model.document;
 			modelRoot = modelDoc.createRoot();
 			modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
 			modelDocFrag = new ModelDocumentFragment( [ new ModelText( 'bar' ) ] );
@@ -627,7 +628,7 @@ describe( 'debug tools', () => {
 			const op = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), [ new ModelText( 'foo' ) ], 0 );
 			delta.addOperation( op );
 
-			modelDoc.applyOperation( op );
+			model.applyOperation( op );
 
 			expect( log.calledWithExactly( 'Applying InsertOperation( 0 ): #foo -> main [ 0 ]' ) ).to.be.true;
 		} );
@@ -635,7 +636,8 @@ describe( 'debug tools', () => {
 
 	describe( 'should provide tree printing tools', () => {
 		it( 'for model', () => {
-			const modelDoc = new ModelDocument();
+			const model = new Model();
+			const modelDoc = model.document;
 			const modelRoot = modelDoc.createRoot();
 
 			modelRoot.appendChildren( [
@@ -795,26 +797,27 @@ describe( 'debug tools', () => {
 		} );
 
 		it( 'should store model and view state after each applied operation', () => {
-			const model = editor.document;
-			const modelRoot = model.getRoot();
-			const view = editor.editing.view;
+			const model = editor.model;
+			const modelDoc = model.document;
+			const modelRoot = modelDoc.getRoot();
+			const viewDoc = editor.editing.view;
 
 			const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), 0 );
 			model.applyOperation( wrapInDelta( insert ) );
 
-			const graveyard = model.graveyard;
+			const graveyard = modelDoc.graveyard;
 			const remove = new RemoveOperation( ModelPosition.createAt( modelRoot, 1 ), 2, ModelPosition.createAt( graveyard, 0 ), 1 );
 			model.applyOperation( wrapInDelta( remove ) );
 
 			log.reset();
 
-			model.log( 0 );
+			modelDoc.log( 0 );
 			expectLog(
 				'<$graveyard></$graveyard>' +
 				'\n<main></main>'
 			);
 
-			model.log( 1 );
+			modelDoc.log( 1 );
 			expectLog(
 				'<$graveyard></$graveyard>' +
 				'\n<main>' +
@@ -822,7 +825,7 @@ describe( 'debug tools', () => {
 				'\n</main>'
 			);
 
-			model.log( 2 );
+			modelDoc.log( 2 );
 			expectLog(
 				'<$graveyard>' +
 				'\n\too' +
@@ -832,7 +835,7 @@ describe( 'debug tools', () => {
 				'\n</main>'
 			);
 
-			model.log();
+			modelDoc.log();
 			expectLog(
 				'<$graveyard>' +
 				'\n\too' +
@@ -842,74 +845,76 @@ describe( 'debug tools', () => {
 				'\n</main>'
 			);
 
-			view.log( 0 );
+			viewDoc.log( 0 );
 			expectLog(
 				'<div></div>'
 			);
 
-			view.log( 1 );
+			viewDoc.log( 1 );
 			expectLog(
 				'<div>' +
 				'\n\tfoobar' +
 				'\n</div>'
 			);
 
-			view.log( 2 );
+			viewDoc.log( 2 );
 			expectLog(
 				'<div>' +
 				'\n\tfbar' +
 				'\n</div>'
 			);
 
-			sinon.spy( model, 'log' );
-			sinon.spy( view, 'log' );
+			sinon.spy( modelDoc, 'log' );
+			sinon.spy( viewDoc, 'log' );
 
 			editor.logModel( 1 );
-			expect( model.log.calledWithExactly( 1 ) ).to.be.true;
+			expect( modelDoc.log.calledWithExactly( 1 ), 1 ).to.be.true;
 
 			editor.logView( 2 );
-			expect( view.log.calledWithExactly( 2 ) ).to.be.true;
+			expect( viewDoc.log.calledWithExactly( 2 ), 2 ).to.be.true;
 
-			model.log.reset();
-			view.log.reset();
+			modelDoc.log.reset();
+			viewDoc.log.reset();
 
 			editor.logModel();
-			expect( model.log.calledWithExactly( 2 ) ).to.be.true;
+			expect( modelDoc.log.calledWithExactly( 2 ), 3 ).to.be.true;
 
-			model.log.reset();
-			view.log.reset();
+			modelDoc.log.reset();
+			viewDoc.log.reset();
 
 			editor.logDocuments();
-			expect( model.log.calledWithExactly( 2 ) ).to.be.true;
-			expect( view.log.calledWithExactly( 2 ) ).to.be.true;
+			expect( modelDoc.log.calledWithExactly( 2 ), 4 ).to.be.true;
+			expect( viewDoc.log.calledWithExactly( 2 ), 5 ).to.be.true;
 
-			model.log.reset();
-			view.log.reset();
+			modelDoc.log.reset();
+			viewDoc.log.reset();
 
 			editor.logDocuments( 1 );
-			expect( model.log.calledWithExactly( 1 ) ).to.be.true;
-			expect( view.log.calledWithExactly( 1 ) ).to.be.true;
+			expect( modelDoc.log.calledWithExactly( 1 ), 6 ).to.be.true;
+			expect( viewDoc.log.calledWithExactly( 1 ), 7 ).to.be.true;
 		} );
 
 		it( 'should remove old states', () => {
-			const model = editor.document;
-			const modelRoot = model.getRoot();
+			const model = editor.model;
+			const modelDoc = model.document;
+			const modelRoot = model.document.getRoot();
 
 			for ( let i = 0; i < 25; i++ ) {
-				const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), model.version );
+				const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), modelDoc.version );
 				model.applyOperation( wrapInDelta( insert ) );
 			}
 
-			model.log( 0 );
+			modelDoc.log( 0 );
 			expectLog( 'Tree log unavailable for given version: 0' );
 		} );
 	} );
 
 	describe( 'should provide methods for delta replayer', () => {
 		it( 'getAppliedDeltas()', () => {
-			const modelDoc = new ModelDocument();
+			const model = new Model();
+			const modelDoc = model.document;
 
-			expect( modelDoc.getAppliedDeltas() ).to.equal( '' );
+			expect( model.getAppliedDeltas() ).to.equal( '' );
 
 			const otherRoot = modelDoc.createRoot( '$root', 'otherRoot' );
 			const firstEle = new ModelElement( 'paragraph' );
@@ -925,16 +930,17 @@ describe( 'debug tools', () => {
 			delta.addOperation( move );
 			delta.addOperation( remove );
 
-			modelDoc.applyOperation( move );
-			modelDoc.applyOperation( remove );
+			model.applyOperation( move );
+			model.applyOperation( remove );
 
-			const stringifiedDeltas = modelDoc.getAppliedDeltas();
+			const stringifiedDeltas = model.getAppliedDeltas();
 
 			expect( stringifiedDeltas ).to.equal( JSON.stringify( delta.toJSON() ) );
 		} );
 
 		it( 'createReplayer()', () => {
-			const modelDoc = new ModelDocument();
+			const model = new Model();
+			const modelDoc = model.document;
 
 			const otherRoot = modelDoc.createRoot( '$root', 'otherRoot' );
 			const firstEle = new ModelElement( 'paragraph' );
@@ -952,17 +958,18 @@ describe( 'debug tools', () => {
 
 			const stringifiedDeltas = JSON.stringify( delta.toJSON() );
 
-			const deltaReplayer = modelDoc.createReplayer( stringifiedDeltas );
+			const deltaReplayer = model.createReplayer( stringifiedDeltas );
 
 			expect( deltaReplayer.getDeltasToReplay() ).to.deep.equal( [ JSON.parse( stringifiedDeltas ) ] );
 		} );
 	} );
 
 	describe( 'should provide debug tools for delta transformation', () => {
-		let document, root, otherRoot;
+		let model, document, root, otherRoot;
 
 		beforeEach( () => {
-			document = new ModelDocument();
+			model = new Model();
+			document = model.document;
 			root = document.createRoot();
 			otherRoot = document.createRoot( 'other', 'other' );
 		} );
@@ -1063,7 +1070,11 @@ describe( 'debug tools', () => {
 			const firstResultWithoutHistory = result[ 0 ].clone();
 			delete firstResultWithoutHistory.history;
 
-			result = deltaTransform.transform( result[ 0 ], deltaC, { isStrong: true, document, wasAffected: new Map() } );
+			result = deltaTransform.transform( result[ 0 ], deltaC, {
+				isStrong: true,
+				document,
+				wasAffected: new Map()
+			} );
 			expect( result[ 0 ].history ).not.to.be.undefined;
 			expect( result[ 0 ].history.length ).to.equal( 2 );
 
@@ -1098,7 +1109,11 @@ describe( 'debug tools', () => {
 			deltaC.addOperation( opC );
 
 			let original = deltaTransform.transform( deltaA, deltaB, { document, wasAffected: new Map() } );
-			original = deltaTransform.transform( original[ 0 ], deltaC, { isStrong: true, document, wasAffected: new Map() } )[ 0 ];
+			original = deltaTransform.transform( original[ 0 ], deltaC, {
+				isStrong: true,
+				document,
+				wasAffected: new Map()
+			} )[ 0 ];
 
 			const history = original.history;
 

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

@@ -4,7 +4,7 @@
  */
 
 import { stringify, parse, getData, setData } from '../../src/dev-utils/model';
-import Document from '../../src/model/document';
+import Model from '../../src/model/model';
 import DocumentFragment from '../../src/model/documentfragment';
 import Element from '../../src/model/element';
 import Text from '../../src/model/text';
@@ -13,31 +13,32 @@ import Position from '../../src/model/position';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 
 describe( 'model test utils', () => {
-	let document, root, selection, sandbox;
+	let model, document, root, selection, sandbox;
 
 	beforeEach( () => {
-		document = new Document();
+		model = new Model();
+		document = model.document;
 		root = document.createRoot();
 		selection = document.selection;
 		sandbox = sinon.sandbox.create();
 		selection.removeAllRanges();
 
-		document.schema.registerItem( 'a', '$inline' );
-		document.schema.allow( { name: 'a', inside: '$root' } );
-		document.schema.allow( { name: 'a', inside: '$root', attributes: [ 'bar', 'car', 'foo' ] } );
+		model.schema.registerItem( 'a', '$inline' );
+		model.schema.allow( { name: 'a', inside: '$root' } );
+		model.schema.allow( { name: 'a', inside: '$root', attributes: [ 'bar', 'car', 'foo' ] } );
 
-		document.schema.registerItem( 'b', '$inline' );
-		document.schema.allow( { name: 'b', inside: '$root' } );
-		document.schema.allow( { name: 'b', inside: '$root', attributes: [ 'barFoo', 'fooBar', 'x' ] } );
+		model.schema.registerItem( 'b', '$inline' );
+		model.schema.allow( { name: 'b', inside: '$root' } );
+		model.schema.allow( { name: 'b', inside: '$root', attributes: [ 'barFoo', 'fooBar', 'x' ] } );
 
-		document.schema.registerItem( 'c', '$inline' );
-		document.schema.allow( { name: 'c', inside: '$root' } );
+		model.schema.registerItem( 'c', '$inline' );
+		model.schema.allow( { name: 'c', inside: '$root' } );
 
-		document.schema.registerItem( 'paragraph', '$block' );
-		document.schema.allow( { name: '$text', inside: '$root' } );
-		document.schema.allow( { name: '$text', inside: 'a' } );
-		document.schema.allow( { name: '$text', inside: 'b' } );
-		document.schema.allow( { name: 'c', inside: 'b' } );
+		model.schema.registerItem( 'paragraph', '$block' );
+		model.schema.allow( { name: '$text', inside: '$root' } );
+		model.schema.allow( { name: '$text', inside: 'a' } );
+		model.schema.allow( { name: '$text', inside: 'b' } );
+		model.schema.allow( { name: 'c', inside: 'b' } );
 	} );
 
 	afterEach( () => {
@@ -49,7 +50,7 @@ describe( 'model test utils', () => {
 			const stringifySpy = sandbox.spy( getData, '_stringify' );
 			root.appendChildren( new Element( 'b', null, new Text( 'btext' ) ) );
 
-			expect( getData( document, { withoutSelection: true } ) ).to.equal( '<b>btext</b>' );
+			expect( getData( model, { withoutSelection: true } ) ).to.equal( '<b>btext</b>' );
 			sinon.assert.calledOnce( stringifySpy );
 			sinon.assert.calledWithExactly( stringifySpy, root );
 		} );
@@ -59,7 +60,7 @@ describe( 'model test utils', () => {
 			root.appendChildren( new Element( 'b', null, new Text( 'btext' ) ) );
 			document.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
 
-			expect( getData( document ) ).to.equal( '[<b>btext</b>]' );
+			expect( getData( model ) ).to.equal( '[<b>btext</b>]' );
 			sinon.assert.calledOnce( stringifySpy );
 			sinon.assert.calledWithExactly( stringifySpy, root, document.selection );
 		} );
@@ -67,7 +68,7 @@ describe( 'model test utils', () => {
 		it( 'should throw an error when passing invalid document', () => {
 			expect( () => {
 				getData( { invalid: 'document' } );
-			} ).to.throw( TypeError, 'Document needs to be an instance of module:engine/model/document~Document.' );
+			} ).to.throw( TypeError, 'Model needs to be an instance of module:engine/model/model~Model.' );
 		} );
 	} );
 
@@ -77,9 +78,9 @@ describe( 'model test utils', () => {
 			const options = {};
 			const data = '<b>btext</b>text';
 
-			setData( document, data, options );
+			setData( model, data, options );
 
-			expect( getData( document, { withoutSelection: true } ) ).to.equal( data );
+			expect( getData( model, { withoutSelection: true } ) ).to.equal( data );
 			sinon.assert.calledOnce( parseSpy );
 			const args = parseSpy.firstCall.args;
 			expect( args[ 0 ] ).to.equal( data );
@@ -90,9 +91,9 @@ describe( 'model test utils', () => {
 			const options = {};
 			const data = '[<b>btext</b>]';
 
-			setData( document, data, options );
+			setData( model, data, options );
 
-			expect( getData( document ) ).to.equal( data );
+			expect( getData( model ) ).to.equal( data );
 			sinon.assert.calledOnce( parseSpy );
 			const args = parseSpy.firstCall.args;
 			expect( args[ 0 ] ).to.equal( data );
@@ -139,41 +140,41 @@ describe( 'model test utils', () => {
 		} );
 
 		it( 'should insert backward selection', () => {
-			setData( document, '<b>[foo bar</b>]', { lastRangeBackward: true } );
+			setData( model, '<b>[foo bar</b>]', { lastRangeBackward: true } );
 
-			expect( getData( document ) ).to.equal( '<b>[foo bar</b>]' );
+			expect( getData( model ) ).to.equal( '<b>[foo bar</b>]' );
 			expect( document.selection.isBackward ).to.true;
 		} );
 
 		it( 'should throw an error when passing invalid document', () => {
 			expect( () => {
 				setData( { invalid: 'document' } );
-			} ).to.throw( TypeError, 'Document needs to be an instance of module:engine/model/document~Document.' );
+			} ).to.throw( TypeError, 'Model needs to be an instance of module:engine/model/model~Model.' );
 		} );
 
 		it( 'should set attributes to the selection', () => {
-			setData( document, '<b>[foo bar]</b>', { selectionAttributes: { foo: 'bar' } } );
+			setData( model, '<b>[foo bar]</b>', { selectionAttributes: { foo: 'bar' } } );
 
 			expect( document.selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
 		} );
 
 		// #815.
 		it( 'should work in a special root', () => {
-			const document = new Document();
+			const model = new Model();
 
-			document.schema.registerItem( 'textOnly' );
-			document.schema.allow( { name: '$text', inside: 'textOnly' } );
-			document.createRoot( 'textOnly', 'textOnly' );
+			model.schema.registerItem( 'textOnly' );
+			model.schema.allow( { name: '$text', inside: 'textOnly' } );
+			model.document.createRoot( 'textOnly', 'textOnly' );
 
-			setData( document, 'a[b]c', { rootName: 'textOnly' } );
-			expect( getData( document, { rootName: 'textOnly' } ) ).to.equal( 'a[b]c' );
+			setData( model, 'a[b]c', { rootName: 'textOnly' } );
+			expect( getData( model, { rootName: 'textOnly' } ) ).to.equal( 'a[b]c' );
 		} );
 
 		function test( data, expected ) {
 			expected = expected || data;
 
-			setData( document, data );
-			expect( getData( document ) ).to.equal( expected );
+			setData( model, data );
+			expect( getData( model ) ).to.equal( expected );
 		}
 	} );
 
@@ -497,31 +498,31 @@ describe( 'model test utils', () => {
 
 		it( 'throws when invalid XML', () => {
 			expect( () => {
-				parse( '<a><b></a></b>', document.schema, document.batch() );
+				parse( '<a><b></a></b>', model.schema );
 			} ).to.throw( Error, /Parse error/ );
 		} );
 
 		it( 'throws when try to set element not registered in schema', () => {
 			expect( () => {
-				parse( '<xyz></xyz>', document.schema, document.batch() );
+				parse( '<xyz></xyz>', model.schema );
 			} ).to.throw( Error, 'Element \'xyz\' not allowed in context ["$root"].' );
 		} );
 
 		it( 'throws when try to set text directly to $root without registering it', () => {
-			const document = new Document();
+			const model = new Model();
 
 			expect( () => {
-				parse( 'text', document.schema, document.batch() );
+				parse( 'text', model.schema );
 			} ).to.throw( Error, 'Element \'$text\' not allowed in context ["$root"].' );
 		} );
 
 		it( 'converts data in the specified context', () => {
-			const doc = new Document();
-			doc.schema.registerItem( 'foo' );
-			doc.schema.allow( { name: '$text', inside: 'foo' } );
+			const model = new Model();
+			model.schema.registerItem( 'foo' );
+			model.schema.allow( { name: '$text', inside: 'foo' } );
 
 			expect( () => {
-				parse( 'text', doc.schema, doc.batch(), { context: [ 'foo' ] } );
+				parse( 'text', model.schema, { context: [ 'foo' ] } );
 			} ).to.not.throw();
 		} );
 
@@ -556,7 +557,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets selection attributes', () => {
-				const result = parse( 'foo[]bar', document.schema, document.batch(), { selectionAttributes: {
+				const result = parse( 'foo[]bar', model.schema, { selectionAttributes: {
 					bold: true,
 					italic: true
 				} } );
@@ -577,7 +578,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets selection with attribute containing an element', () => {
-				const result = parse( 'x[<a></a>]', document.schema, document.batch(), { selectionAttributes: {
+				const result = parse( 'x[<a></a>]', model.schema, { selectionAttributes: {
 					bold: true
 				} } );
 
@@ -586,7 +587,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets a backward selection containing an element', () => {
-				const result = parse( 'x[<a></a>]', document.schema, document.batch(), {
+				const result = parse( 'x[<a></a>]', model.schema, {
 					lastRangeBackward: true
 				} );
 
@@ -599,7 +600,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, document.batch(), {
+				const result = parse( '<$text bold="true">fo[o</$text>ba]r', model.schema, {
 					selectionAttributes: { bold: true }
 				} );
 
@@ -609,34 +610,34 @@ describe( 'model test utils', () => {
 
 			it( 'throws when missing selection start', () => {
 				expect( () => {
-					parse( 'foo]', document.schema, document.batch() );
-				} ).to.throw( Error );
+					parse( 'foo]', model.schema );
+				} ).to.throw( Error, /^Parse error/ );
 			} );
 
 			it( 'throws when missing selection end', () => {
 				expect( () => {
-					parse( '[foo', document.schema, document.batch() );
-				} ).to.throw( Error );
+					parse( '[foo', model.schema );
+				} ).to.throw( Error, /^Parse error/ );
 			} );
 		} );
 
 		function test( title, options ) {
 			it( title, () => {
 				const output = options.output || options.data;
-				const data = parse( options.data, document.schema, document.batch() );
-				let model, selection;
+				const data = parse( options.data, model.schema );
+				let converted, selection;
 
 				if ( data.selection && data.model ) {
-					model = data.model;
+					converted = data.model;
 					selection = data.selection;
 				} else {
-					model = data;
+					converted = data;
 				}
 
-				expect( stringify( model, selection ) ).to.equal( output );
+				expect( stringify( converted, selection ) ).to.equal( output );
 
 				if ( options.check ) {
-					options.check( model, selection );
+					options.check( converted, selection );
 				}
 			} );
 		}

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

@@ -32,8 +32,7 @@ class FancyWidget extends Plugin {
 
 	init() {
 		const editor = this.editor;
-		const doc = editor.document;
-		const schema = doc.schema;
+		const schema = editor.model.schema;
 		const data = editor.data;
 		const editing = editor.editing;
 
@@ -101,9 +100,9 @@ ClassicEditor.create( global.document.querySelector( '#editor' ), {
 		} );
 
 		document.getElementById( 'remove-markers' ).addEventListener( 'mousedown', evt => {
-			const markers = editor.document.markers;
+			const markers = editor.model.markers;
 
-			editor.document.enqueueChanges( () => {
+			editor.model.change( () => {
 				for ( const marker of markers ) {
 					markers.remove( marker );
 				}
@@ -117,18 +116,14 @@ ClassicEditor.create( global.document.querySelector( '#editor' ), {
 	} );
 
 function addMarker( editor, color ) {
-	const model = editor.document;
-
-	editor.document.enqueueChanges( () => {
-		const range = ModelRange.createFromRange( model.selection.getFirstRange() );
-		model.markers.set( 'marker:' + color, range );
+	editor.model.change( () => {
+		const range = ModelRange.createFromRange( editor.model.document.selection.getFirstRange() );
+		editor.model.markers.set( 'marker:' + color, range );
 	} );
 }
 
 function removeMarker( editor, color ) {
-	const model = editor.document;
-
-	editor.document.enqueueChanges( () => {
-		model.markers.remove( 'marker:' + color );
+	editor.model.change( () => {
+		editor.model.markers.remove( 'marker:' + color );
 	} );
 }

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

@@ -30,7 +30,7 @@ ClassicEditor
 	} )
 	.then( editor => {
 		window.editor = editor;
-		model = window.editor.editing.model;
+		model = editor.model;
 
 		buildModelConverter().for( editor.editing.modelToView )
 			.fromMarker( 'highlight' )
@@ -73,8 +73,8 @@ ClassicEditor
 			moveSelectionByOffset( 1 );
 		} );
 
-		model.enqueueChanges( () => {
-			const root = model.getRoot();
+		model.change( () => {
+			const root = model.document.getRoot();
 			const range = new Range( new Position( root, [ 0, 10 ] ), new Position( root, [ 0, 16 ] ) );
 			const name = 'highlight:yellow:' + uid();
 
@@ -91,8 +91,8 @@ function uid() {
 }
 
 function addHighlight( color ) {
-	model.enqueueChanges( () => {
-		const range = Range.createFromRange( model.selection.getFirstRange() );
+	model.change( () => {
+		const range = Range.createFromRange( model.document.selection.getFirstRange() );
 		const name = 'highlight:' + color + ':' + uid();
 
 		markerNames.push( name );
@@ -101,8 +101,8 @@ function addHighlight( color ) {
 }
 
 function removeHighlight() {
-	model.enqueueChanges( () => {
-		const pos = model.selection.getFirstPosition();
+	model.change( () => {
+		const pos = model.document.selection.getFirstPosition();
 
 		for ( let i = 0; i < markerNames.length; i++ ) {
 			const name = markerNames[ i ];
@@ -120,22 +120,22 @@ function removeHighlight() {
 }
 
 function moveSelectionToStart() {
-	const range = model.selection.getFirstRange();
+	const range = model.document.selection.getFirstRange();
 
 	if ( range.isFlat ) {
-		model.enqueueChanges( () => {
-			model.batch().move( range, new Position( model.getRoot(), [ 0, 0 ] ) );
+		model.change( writer => {
+			writer.move( range, new Position( model.document.getRoot(), [ 0, 0 ] ) );
 		} );
 	}
 }
 
 function moveSelectionByOffset( offset ) {
-	const range = model.selection.getFirstRange();
+	const range = model.document.selection.getFirstRange();
 	const pos = offset < 0 ? range.start : range.end;
 
 	if ( range.isFlat ) {
-		model.enqueueChanges( () => {
-			model.batch().move( range, pos.getShiftedBy( offset ) );
+		model.change( writer => {
+			writer.move( range, pos.getShiftedBy( offset ) );
 		} );
 	}
 }

+ 3 - 4
packages/ckeditor5-engine/tests/manual/nestededitable.js

@@ -23,11 +23,10 @@ import './nestededitable.css';
 class NestedEditable extends Plugin {
 	init() {
 		const editor = this.editor;
-		const document = editor.document;
 		const editing = editor.editing;
 		const viewDocument = editing.view;
 		const data = editor.data;
-		const schema = document.schema;
+		const schema = editor.model.schema;
 
 		schema.registerItem( 'figure' );
 		schema.registerItem( 'figcaption' );
@@ -75,7 +74,7 @@ ClassicEditor
 		toolbar: [ 'undo', 'redo' ]
 	} )
 	.then( editor => {
-		editor.document.on( 'changesDone', () => {
+		editor.model.document.on( 'changesDone', () => {
 			printModelContents( editor );
 		} );
 
@@ -87,5 +86,5 @@ ClassicEditor
 
 const modelDiv = global.document.querySelector( '#model' );
 function printModelContents( editor ) {
-	modelDiv.innerText = getData( editor.document );
+	modelDiv.innerText = getData( editor.model );
 }

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

@@ -19,7 +19,7 @@ ClassicEditor
 	.then( editor => {
 		window.editor = editor;
 
-		const schema = editor.document.schema;
+		const schema = editor.model.schema;
 
 		schema.disallow( { name: '$text', attributes: [ 'linkHref', 'italic' ], inside: 'heading1' } );
 		schema.disallow( { name: '$text', attributes: [ 'italic' ], inside: 'heading2' } );

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

@@ -27,8 +27,8 @@ ClassicEditor
 			const selectionExists = domSelection && domSelection.anchorNode;
 
 			console.log( editor.editing.view.getDomRoot().innerHTML.replace( /\u200b/g, '@' ) );
-			console.log( 'selection.hasAttribute( italic ):', editor.document.selection.hasAttribute( 'italic' ) );
-			console.log( 'selection.hasAttribute( bold ):', editor.document.selection.hasAttribute( 'bold' ) );
+			console.log( 'selection.hasAttribute( italic ):', editor.model.document.selection.hasAttribute( 'italic' ) );
+			console.log( 'selection.hasAttribute( bold ):', editor.model.document.selection.hasAttribute( 'bold' ) );
 			console.log( 'selection anchor\'s parentNode:', selectionExists ? domSelection.anchorNode.parentNode : 'no DOM selection' );
 		}, 2000 );
 	} )

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

@@ -31,7 +31,7 @@ class Link extends Plugin {
 		const editing = editor.editing;
 
 		// Allow bold attribute on all inline nodes.
-		editor.document.schema.allow( { name: '$inline', attributes: [ 'link' ] } );
+		editor.model.schema.allow( { name: '$inline', attributes: [ 'link' ] } );
 
 		// Build converter from model to view for data and editing pipelines.
 		buildModelConverter().for( data.modelToView, editing.modelToView )
@@ -49,7 +49,7 @@ const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b(
 
 class AutoLinker extends Plugin {
 	init() {
-		this.editor.document.on( 'change', ( event, type, changes, batch ) => {
+		this.editor.model.document.on( 'change', ( event, type, changes, batch ) => {
 			if ( type != 'insert' ) {
 				return;
 			}
@@ -73,14 +73,13 @@ class AutoLinker extends Plugin {
 					return;
 				}
 
-				const doc = this.editor.document;
 				const url = matchedUrl[ 0 ];
 				const offset = _getLastPathPart( currentValue.nextPosition.path ) + matchedUrl.index;
 				const livePos = LivePosition.createFromParentAndOffset( currentValue.item.parent, offset );
 
-				doc.enqueueChanges( () => {
+				this.editor.model.enqueueChange( batch, writer => {
 					const urlRange = Range.createFromPositionAndShift( livePos, url.length );
-					batch.setAttribute( 'link', url, urlRange );
+					writer.setAttribute( 'link', url, urlRange );
 				} );
 			}
 		} );

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

@@ -21,7 +21,7 @@ ClassicEditor
 	.then( editor => {
 		window.editor = editor;
 
-		const sel = editor.document.selection;
+		const sel = editor.model.document.selection;
 
 		sel.on( 'change', ( evt, data ) => {
 			const date = new Date();

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

@@ -19,7 +19,7 @@ ClassicEditor
 		window.editor = editor;
 
 		editor.editing.view.on( 'selectionChange', () => {
-			editor.document.enqueueChanges( () => {
+			editor.model.change( () => {
 			} );
 			console.log( 'selectionChange', ( new Date() ).getTime() );
 		} );

+ 1 - 1
packages/ckeditor5-engine/tests/model/_utils/utils.js

@@ -39,7 +39,7 @@ export function getNodesAndText( range ) {
 }
 
 /**
- * Returns object JSON representation. It pases an object by JSON.stringify and JSON.parse functions.
+ * Returns object JSON representation. It passes an object by JSON.stringify and JSON.parse functions.
  *
  * @param {Object|Array} object
  */

+ 10 - 1874
packages/ckeditor5-engine/tests/model/batch.js

@@ -5,33 +5,18 @@
 
 import Batch from '../../src/model/batch';
 import Delta from '../../src/model/delta/delta';
-import InsertDelta from '../../src/model/delta/insertdelta';
-import WeakInsertDelta from '../../src/model/delta/weakinsertdelta';
-
 import Operation from '../../src/model/operation/operation';
 
-import Document from '../../src/model/document';
-import DocumentFragment from '../../src/model/documentfragment';
-import Element from '../../src/model/element';
-import Text from '../../src/model/text';
-import Position from '../../src/model/position';
-import Range from '../../src/model/range';
-
-import count from '@ckeditor/ckeditor5-utils/src/count';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
-import { getNodesAndText } from '../../tests/model/_utils/utils';
-
 describe( 'Batch', () => {
 	describe( 'type', () => {
-		it( 'should default to "default"', () => {
-			const batch = new Batch( new Document() );
+		it( 'should default be "default"', () => {
+			const batch = new Batch();
 
 			expect( batch.type ).to.equal( 'default' );
 		} );
 
 		it( 'should be set to the value set in constructor', () => {
-			const batch = new Batch( new Document(), 'ignore' );
+			const batch = new Batch( 'ignore' );
 
 			expect( batch.type ).to.equal( 'ignore' );
 		} );
@@ -39,7 +24,7 @@ describe( 'Batch', () => {
 
 	describe( 'baseVersion', () => {
 		it( 'should return base version of first delta from the batch', () => {
-			const batch = new Batch( new Document() );
+			const batch = new Batch();
 			const delta = new Delta();
 			const operation = new Operation( 2 );
 			delta.addOperation( operation );
@@ -49,7 +34,7 @@ describe( 'Batch', () => {
 		} );
 
 		it( 'should return null if there are no deltas in batch', () => {
-			const batch = new Batch( new Document() );
+			const batch = new Batch();
 
 			expect( batch.baseVersion ).to.be.null;
 		} );
@@ -57,7 +42,7 @@ describe( 'Batch', () => {
 
 	describe( 'addDelta()', () => {
 		it( 'should add delta to the batch', () => {
-			const batch = new Batch( new Document() );
+			const batch = new Batch();
 			const deltaA = new Delta();
 			const deltaB = new Delta();
 			batch.addDelta( deltaA );
@@ -71,14 +56,13 @@ describe( 'Batch', () => {
 
 	describe( 'getOperations()', () => {
 		it( 'should return collection of operations from all deltas', () => {
-			const doc = new Document();
-			const batch = new Batch( doc );
+			const batch = new Batch();
 			const deltaA = new Delta();
 			const deltaB = new Delta();
 			const ops = [
-				new Operation( doc.version ),
-				new Operation( doc.version + 1 ),
-				new Operation( doc.version + 2 )
+				new Operation( 0 ),
+				new Operation( 1 ),
+				new Operation( 2 )
 			];
 
 			batch.addDelta( deltaA );
@@ -91,1852 +75,4 @@ describe( 'Batch', () => {
 			expect( batch.getOperations() ).to.have.property( 'next' );
 		} );
 	} );
-
-	describe( 'createText()', () => {
-		let doc, batch;
-
-		beforeEach( () => {
-			doc = new Document();
-			batch = doc.batch();
-		} );
-
-		it( 'should create text node', () => {
-			const text = batch.createText( 'foo' );
-
-			expect( text ).to.instanceof( Text );
-			expect( text.data ).to.equal( 'foo' );
-			expect( Array.from( text.getAttributes() ) ).to.length( 0 );
-		} );
-
-		it( 'should create text with attributes', () => {
-			const text = batch.createText( 'foo', { foo: 'bar', biz: 'baz' } );
-
-			expect( Array.from( text.getAttributes() ) ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'biz', 'baz' ] ] );
-		} );
-	} );
-
-	describe( 'createElement()', () => {
-		let doc, batch;
-
-		beforeEach( () => {
-			doc = new Document();
-			batch = doc.batch();
-		} );
-
-		it( 'should create element', () => {
-			const element = batch.createElement( 'foo' );
-
-			expect( element ).to.instanceof( Element );
-			expect( element.name ).to.equal( 'foo' );
-			expect( Array.from( element.getAttributes() ) ).to.length( 0 );
-		} );
-
-		it( 'should create element with attributes', () => {
-			const element = batch.createText( 'foo', { foo: 'bar', biz: 'baz' } );
-
-			expect( Array.from( element.getAttributes() ) ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'biz', 'baz' ] ] );
-		} );
-	} );
-
-	describe( 'createDocumentFragment()', () => {
-		let doc, batch;
-
-		beforeEach( () => {
-			doc = new Document();
-			batch = doc.batch();
-		} );
-
-		it( 'should create element', () => {
-			const element = batch.createDocumentFragment();
-
-			expect( element ).to.instanceof( DocumentFragment );
-		} );
-	} );
-
-	describe( 'insert()', () => {
-		let doc, batch;
-
-		beforeEach( () => {
-			doc = new Document();
-			batch = doc.batch();
-		} );
-
-		it( 'should insert node at given position', () => {
-			const parent = batch.createDocumentFragment();
-			const child = batch.createElement( 'child' );
-			const textChild = batch.createText( 'textChild' );
-
-			batch.insert( child, new Position( parent, [ 0 ] ) );
-			batch.insert( textChild, new Position( parent, [ 1 ] ) );
-
-			expect( Array.from( parent ) ).to.deep.equal( [ child, textChild ] );
-		} );
-
-		it( 'should insert node at the beginning of given element', () => {
-			const parent = batch.createDocumentFragment();
-			const child1 = batch.createElement( 'child' );
-			const child2 = batch.createElement( 'child' );
-
-			batch.insert( child1, parent );
-			batch.insert( child2, parent );
-
-			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child2, child1 ] );
-		} );
-
-		it( 'should insert node at the end of given element', () => {
-			const parent = batch.createDocumentFragment();
-			const child1 = batch.createElement( 'child' );
-			const child2 = batch.createElement( 'child' );
-
-			batch.insert( child1, parent );
-			batch.insert( child2, parent, 'end' );
-
-			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2 ] );
-		} );
-
-		it( 'should insert node at the given offset of given element', () => {
-			const parent = batch.createDocumentFragment();
-			const child1 = batch.createElement( 'child' );
-			const child2 = batch.createElement( 'child' );
-			const child3 = batch.createElement( 'child' );
-
-			batch.insert( child3, parent );
-			batch.insert( child1, parent );
-			batch.insert( child2, parent, 1 );
-
-			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] );
-		} );
-
-		it( 'should insert node before the given node', () => {
-			const parent = batch.createDocumentFragment();
-			const child1 = batch.createElement( 'child' );
-			const child2 = batch.createElement( 'child' );
-			const child3 = batch.createElement( 'child' );
-
-			batch.insert( child3, parent );
-			batch.insert( child1, parent );
-			batch.insert( child2, child3, 'before' );
-
-			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] );
-		} );
-
-		it( 'should insert node after the given node', () => {
-			const parent = batch.createDocumentFragment();
-			const child1 = batch.createElement( 'child' );
-			const child2 = batch.createElement( 'child' );
-			const child3 = batch.createElement( 'child' );
-
-			batch.insert( child3, parent );
-			batch.insert( child1, parent );
-			batch.insert( child2, child1, 'after' );
-
-			expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] );
-		} );
-
-		it( 'should create proper delta for inserting element', () => {
-			const parent = batch.createDocumentFragment();
-			const element = batch.createElement( 'child' );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.insert( element, parent );
-
-			sinon.assert.calledOnce( spy );
-			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta );
-			expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta );
-			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should create proper delta for inserting text', () => {
-			const parent = batch.createDocumentFragment();
-			const text = batch.createText( 'child' );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.insert( text, parent );
-
-			sinon.assert.calledOnce( spy );
-			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta );
-			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should move element from one parent to the other within the same document (documentA -> documentA)', () => {
-			const rootA = doc.createRoot();
-			const parent1 = batch.createElement( 'parent' );
-			const parent2 = batch.createElement( 'parent' );
-			const node = batch.createText( 'foo' );
-
-			batch.insert( node, parent1 );
-			batch.insert( parent1, rootA );
-			batch.insert( parent2, rootA );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.insert( node, parent2 );
-
-			// Verify result.
-			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
-			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledOnce( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should move element from one parent to the other within the same document (documentA -> documentB)', () => {
-			const rootA = doc.createRoot( '$root', 'A' );
-			const rootB = doc.createRoot( '$root', 'B' );
-			const node = batch.createText( 'foo' );
-
-			batch.insert( node, rootA );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.insert( node, rootB );
-
-			// Verify result.
-			expect( Array.from( rootA.getChildren() ) ).to.deep.equal( [] );
-			expect( Array.from( rootB.getChildren() ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledOnce( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should move element from one parent to the other within the same document (docFragA -> docFragA)', () => {
-			const docFragA = batch.createDocumentFragment();
-			const parent1 = batch.createElement( 'parent' );
-			const parent2 = batch.createElement( 'parent' );
-			const node = batch.createText( 'foo' );
-
-			batch.insert( node, parent1 );
-			batch.insert( parent1, docFragA );
-			batch.insert( parent2, docFragA );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.insert( node, parent2 );
-
-			// Verify result.
-			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
-			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledOnce( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should move element from one parent to the other within different document (document -> docFrag)', () => {
-			const root = doc.createRoot();
-			const docFrag = batch.createDocumentFragment();
-			const node = batch.createText( 'foo' );
-
-			batch.insert( node, root );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.insert( node, docFrag );
-
-			// Verify result.
-			expect( Array.from( root.getChildren() ) ).to.deep.equal( [] );
-			expect( Array.from( docFrag.getChildren() ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledTwice( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should move element from one parent to the other within different document (docFragA -> docFragB)', () => {
-			const docFragA = batch.createDocumentFragment();
-			const docFragB = batch.createDocumentFragment();
-			const node = batch.createText( 'foo' );
-
-			batch.insert( node, docFragA );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.insert( node, docFragB );
-
-			// Verify result.
-			expect( Array.from( docFragA ) ).to.deep.equal( [] );
-			expect( Array.from( docFragB ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledTwice( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'detach' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should transfer markers from given DocumentFragment', () => {
-			const root = doc.createRoot();
-			const docFrag = batch.createDocumentFragment();
-
-			batch.appendText( 'abcd', root );
-			batch.appendElement( 'p', docFrag );
-			batch.insertText( 'foo bar', new Position( docFrag, [ 0, 0 ] ) );
-
-			const marker = new Range( new Position( docFrag, [ 0, 1 ] ), new Position( docFrag, [ 0, 5 ] ) );
-
-			docFrag.markers.set( 'marker', marker );
-
-			batch.insert( docFrag, new Position( root, [ 2 ] ) );
-
-			expect( Array.from( doc.markers ).length ).to.equal( 1 );
-
-			const range = doc.markers.get( 'marker' ).getRange();
-			expect( range.root ).to.equal( root );
-			expect( range.start.path ).to.deep.equal( [ 2, 1 ] );
-			expect( range.end.path ).to.deep.equal( [ 2, 5 ] );
-		} );
-
-		it( 'should set each marker as a separate operation', () => {
-			const spy = sinon.spy();
-			const root = doc.createRoot();
-			const docFrag = batch.createDocumentFragment();
-
-			batch.appendText( 'abcd', root );
-			batch.appendElement( 'p', docFrag );
-			batch.insertText( 'foo bar', new Position( docFrag, [ 0, 0 ] ) );
-
-			const marker1 = new Range( new Position( docFrag, [ 0, 1 ] ), new Position( docFrag, [ 0, 2 ] ) );
-			const marker2 = new Range( new Position( docFrag, [ 0, 5 ] ), new Position( docFrag, [ 0, 6 ] ) );
-
-			docFrag.markers.set( 'marker1', marker1 );
-			docFrag.markers.set( 'marker2', marker2 );
-
-			doc.on( 'change', spy );
-
-			batch.insert( docFrag, new Position( root, [ 2 ] ) );
-
-			sinon.assert.calledThrice( spy );
-			expect( spy.firstCall.args[ 1 ] ).to.equal( 'insert' );
-			expect( spy.secondCall.args[ 1 ] ).to.equal( 'marker' );
-			expect( spy.thirdCall.args[ 1 ] ).to.equal( 'marker' );
-		} );
-	} );
-
-	describe( 'insertText()', () => {
-		let doc, batch;
-
-		beforeEach( () => {
-			doc = new Document();
-			batch = doc.batch();
-		} );
-
-		it( 'should create and insert text node with attributes at given position', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insertText( 'foo', { bar: 'biz' }, new Position( parent, [ 0 ] ) );
-
-			expect( parent.childCount ).to.equal( 1 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Text );
-			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
-		} );
-
-		it( 'should create and insert text node with no attributes at given position', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insertText( 'foo', null, new Position( parent, [ 0 ] ) );
-
-			expect( parent.childCount ).to.equal( 1 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Text );
-			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
-		} );
-
-		it( 'should create and insert text node omitting attributes param', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insertText( 'foo', new Position( parent, [ 0 ] ) );
-
-			expect( parent.childCount ).to.equal( 1 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Text );
-			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
-		} );
-
-		it( 'should create and insert text node at the beginning of given element', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insert( batch.createElement( 'child' ), parent );
-
-			batch.insertText( 'foo', parent );
-
-			expect( parent.childCount ).to.equal( 2 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Text );
-			expect( parent.getChild( 1 ) ).to.instanceof( Element );
-		} );
-
-		it( 'should create and insert text node at the end of given element', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insert( batch.createElement( 'child' ), parent );
-
-			batch.insertText( 'foo', parent, 'end' );
-
-			expect( parent.childCount ).to.equal( 2 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Element );
-			expect( parent.getChild( 1 ) ).to.instanceof( Text );
-		} );
-
-		it( 'should create and insert text node at the given offset of given element', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insert( batch.createElement( 'child' ), parent );
-			batch.insert( batch.createElement( 'child' ), parent );
-
-			batch.insertText( 'foo', parent, 1 );
-
-			expect( parent.childCount ).to.equal( 3 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Element );
-			expect( parent.getChild( 1 ) ).to.instanceof( Text );
-			expect( parent.getChild( 2 ) ).to.instanceof( Element );
-		} );
-
-		it( 'should create and insert text node before the given node', () => {
-			const parent = batch.createDocumentFragment();
-			const child1 = batch.createElement( 'child' );
-			const child2 = batch.createElement( 'child' );
-
-			batch.insert( child1, parent );
-			batch.insert( child2, parent, 'end' );
-
-			batch.insertText( 'foo', child2, 'before' );
-
-			expect( parent.childCount ).to.equal( 3 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Element );
-			expect( parent.getChild( 1 ) ).to.instanceof( Text );
-			expect( parent.getChild( 2 ) ).to.instanceof( Element );
-		} );
-
-		it( 'should create and insert text node after the given node', () => {
-			const parent = batch.createDocumentFragment();
-			const child1 = batch.createElement( 'child' );
-			const child2 = batch.createElement( 'child' );
-
-			batch.insert( child1, parent );
-			batch.insert( child2, parent, 'end' );
-
-			batch.insertText( 'foo', child1, 'after' );
-
-			expect( parent.childCount ).to.equal( 3 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Element );
-			expect( parent.getChild( 1 ) ).to.instanceof( Text );
-			expect( parent.getChild( 2 ) ).to.instanceof( Element );
-		} );
-
-		it( 'should create proper delta', () => {
-			const parent = batch.createDocumentFragment();
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.insertText( 'foo', parent );
-
-			sinon.assert.calledOnce( spy );
-			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta );
-			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-	} );
-
-	describe( 'insertElement()', () => {
-		let doc, batch;
-
-		beforeEach( () => {
-			doc = new Document();
-			batch = doc.batch();
-		} );
-
-		it( 'should create and insert element with attributes at given position', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insertElement( 'foo', { bar: 'biz' }, new Position( parent, [ 0 ] ) );
-
-			expect( parent.childCount ).to.equal( 1 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Element );
-			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
-		} );
-
-		it( 'should create and insert element with no attributes at given position', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insertElement( 'foo', null, new Position( parent, [ 0 ] ) );
-
-			expect( parent.childCount ).to.equal( 1 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Element );
-			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
-		} );
-
-		it( 'should create and insert element with no attributes omitting attributes param', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insertElement( 'foo', new Position( parent, [ 0 ] ) );
-
-			expect( parent.childCount ).to.equal( 1 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Element );
-			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
-		} );
-
-		it( 'should create and insert element at the beginning of given element', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insert( batch.createElement( 'child' ), parent );
-
-			batch.insertElement( 'foo', parent );
-
-			expect( parent.childCount ).to.equal( 2 );
-			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
-			expect( parent.getChild( 1 ).name ).to.equal( 'child' );
-		} );
-
-		it( 'should create and insert element at the end of given element', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insert( batch.createElement( 'child' ), parent );
-
-			batch.insertElement( 'foo', parent, 'end' );
-
-			expect( parent.childCount ).to.equal( 2 );
-			expect( parent.getChild( 0 ).name ).to.equal( 'child' );
-			expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
-		} );
-
-		it( 'should create and insert element at the given offset of given element', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.insert( batch.createElement( 'child1' ), parent );
-			batch.insert( batch.createElement( 'child2' ), parent, 'end' );
-
-			batch.insertElement( 'foo', parent, 1 );
-
-			expect( parent.childCount ).to.equal( 3 );
-			expect( parent.getChild( 0 ).name ).to.equal( 'child1' );
-			expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
-			expect( parent.getChild( 2 ).name ).to.equal( 'child2' );
-		} );
-
-		it( 'should create and insert element before the given node', () => {
-			const parent = batch.createDocumentFragment();
-			const child1 = batch.createElement( 'child1' );
-			const child2 = batch.createElement( 'child2' );
-
-			batch.insert( child1, parent );
-			batch.insert( child2, parent, 'end' );
-
-			batch.insertElement( 'foo', child2, 'before' );
-
-			expect( parent.childCount ).to.equal( 3 );
-			expect( parent.getChild( 0 ).name ).to.equal( 'child1' );
-			expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
-			expect( parent.getChild( 2 ).name ).to.equal( 'child2' );
-		} );
-
-		it( 'should create and insert element after the given node', () => {
-			const parent = batch.createDocumentFragment();
-			const child1 = batch.createElement( 'child1' );
-			const child2 = batch.createElement( 'child2' );
-
-			batch.insert( child1, parent );
-			batch.insert( child2, parent, 'end' );
-
-			batch.insertElement( 'foo', child1, 'after' );
-
-			expect( parent.childCount ).to.equal( 3 );
-			expect( parent.getChild( 0 ).name ).to.equal( 'child1' );
-			expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
-			expect( parent.getChild( 2 ).name ).to.equal( 'child2' );
-		} );
-
-		it( 'should create proper delta', () => {
-			const parent = batch.createDocumentFragment();
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.insertText( 'foo', parent );
-
-			sinon.assert.calledOnce( spy );
-			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta );
-			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-	} );
-
-	describe( 'append()', () => {
-		let doc, batch;
-
-		beforeEach( () => {
-			doc = new Document();
-			batch = doc.batch();
-		} );
-
-		it( 'should insert element at the end of the parent', () => {
-			const parent = doc.batch().createDocumentFragment();
-			const childText = doc.batch().createText( 'foo' );
-			const childElement = doc.batch().createElement( 'foo' );
-
-			batch.append( childText, parent );
-			batch.append( childElement, parent );
-
-			expect( Array.from( parent ) ).to.deep.equal( [ childText, childElement ] );
-		} );
-
-		it( 'should create proper delta', () => {
-			const parent = batch.createDocumentFragment();
-			const text = batch.createText( 'foo' );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.append( text, parent );
-
-			sinon.assert.calledOnce( spy );
-			expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should move element from one parent to the other within the same document (documentA -> documentA)', () => {
-			const rootA = doc.createRoot();
-			const parent1 = batch.createElement( 'parent' );
-			const parent2 = batch.createElement( 'parent' );
-			const node = batch.createText( 'foo' );
-
-			batch.insert( node, parent1 );
-			batch.insert( parent1, rootA );
-			batch.insert( parent2, rootA );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.append( node, parent2 );
-
-			// Verify result.
-			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
-			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledOnce( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should move element from one parent to the other within the same document (documentA -> documentB)', () => {
-			const rootA = doc.createRoot( '$root', 'A' );
-			const rootB = doc.createRoot( '$root', 'B' );
-			const node = batch.createText( 'foo' );
-
-			batch.insert( node, rootA );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.append( node, rootB );
-
-			// Verify result.
-			expect( Array.from( rootA.getChildren() ) ).to.deep.equal( [] );
-			expect( Array.from( rootB.getChildren() ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledOnce( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should move element from one parent to the other within the same document (docFragA -> docFragA)', () => {
-			const docFragA = batch.createDocumentFragment();
-			const parent1 = batch.createElement( 'parent' );
-			const parent2 = batch.createElement( 'parent' );
-			const node = batch.createText( 'foo' );
-
-			batch.insert( node, parent1 );
-			batch.insert( parent1, docFragA );
-			batch.insert( parent2, docFragA );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.append( node, parent2 );
-
-			// Verify result.
-			expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
-			expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledOnce( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should move element from one parent to the other within different document (document -> docFrag)', () => {
-			const root = doc.createRoot();
-			const docFrag = batch.createDocumentFragment();
-			const node = batch.createText( 'foo' );
-
-			batch.insert( node, root );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.append( node, docFrag );
-
-			// Verify result.
-			expect( Array.from( root.getChildren() ) ).to.deep.equal( [] );
-			expect( Array.from( docFrag.getChildren() ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledTwice( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-
-		it( 'should move element from one parent to the other within different document (docFragA -> docFragB)', () => {
-			const docFragA = batch.createDocumentFragment();
-			const docFragB = batch.createDocumentFragment();
-			const node = batch.createText( 'foo' );
-
-			batch.insert( node, docFragA );
-
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.append( node, docFragB );
-
-			// Verify result.
-			expect( Array.from( docFragA ) ).to.deep.equal( [] );
-			expect( Array.from( docFragB ) ).to.deep.equal( [ node ] );
-
-			// Verify deltas and operations.
-			sinon.assert.calledTwice( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'detach' );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-			expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-	} );
-
-	describe( 'appendText()', () => {
-		let doc, batch;
-
-		beforeEach( () => {
-			doc = new Document();
-			batch = doc.batch();
-		} );
-
-		it( 'should create and insert text node with attributes at the end of the parent', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.appendText( 'foo', { bar: 'biz' }, parent );
-			batch.appendText( 'bar', { biz: 'bar' }, parent );
-
-			expect( parent.childCount ).to.equal( 2 );
-			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
-			expect( parent.getChild( 1 ).data ).to.equal( 'bar' );
-			expect( Array.from( parent.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'biz', 'bar' ] ] );
-		} );
-
-		it( 'should create and insert text node with no attributes at the end of the parent', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.appendText( 'foo', null, parent );
-
-			expect( parent.childCount ).to.equal( 1 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Text );
-			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
-		} );
-
-		it( 'should create and insert text node with no attributes omitting attributes param', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.appendText( 'foo', parent );
-
-			expect( parent.childCount ).to.equal( 1 );
-			expect( parent.getChild( 0 ) ).to.instanceof( Text );
-			expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
-		} );
-
-		it( 'should create proper delta and operations', () => {
-			const parent = batch.createDocumentFragment();
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.appendText( 'foo', parent );
-
-			sinon.assert.calledOnce( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.firstCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-	} );
-
-	describe( 'appendElement()', () => {
-		let doc, batch;
-
-		beforeEach( () => {
-			doc = new Document();
-			batch = doc.batch();
-		} );
-
-		it( 'should create and insert element with attributes at the end of the parent', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.appendElement( 'foo', { bar: 'biz' }, parent );
-			batch.appendElement( 'bar', { biz: 'bar' }, parent );
-
-			expect( parent.childCount ).to.equal( 2 );
-			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
-			expect( parent.getChild( 1 ).name ).to.equal( 'bar' );
-			expect( Array.from( parent.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'biz', 'bar' ] ] );
-		} );
-
-		it( 'should create and insert element with no attributes at the end of the parent', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.appendElement( 'foo', null, parent );
-
-			expect( parent.childCount ).to.equal( 1 );
-			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
-		} );
-
-		it( 'should create and insert element with no attributes omitting attributes param', () => {
-			const parent = batch.createDocumentFragment();
-
-			batch.appendElement( 'foo', parent );
-
-			expect( parent.childCount ).to.equal( 1 );
-			expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
-			expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
-		} );
-
-		it( 'should create proper delta and operation', () => {
-			const parent = batch.createDocumentFragment();
-			const spy = sinon.spy( doc, 'applyOperation' );
-
-			batch.appendElement( 'foo', parent );
-
-			sinon.assert.calledOnce( spy );
-			expect( spy.firstCall.args[ 0 ].type ).to.equal( 'insert' );
-			expect( spy.firstCall.args[ 0 ].delta ).to.instanceof( InsertDelta ).to.not.instanceof( WeakInsertDelta );
-			expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
-		} );
-	} );
-
-	describe( 'setAttribute() / removeAttribute()', () => {
-		let batch, doc, root, spy;
-
-		beforeEach( () => {
-			doc = new Document();
-			root = doc.createRoot();
-			batch = doc.batch();
-		} );
-
-		describe( 'change attribute on node', () => {
-			let node, text;
-
-			beforeEach( () => {
-				node = batch.createElement( 'p', { a: 1 } );
-				text = batch.createText( 'c', { a: 1 } );
-
-				batch.append( node, root );
-				batch.append( text, root );
-
-				spy = sinon.spy( doc, 'applyOperation' );
-			} );
-
-			describe( 'setAttribute', () => {
-				it( 'should create the attribute on element', () => {
-					batch.setAttribute( 'b', 2, node );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( node.getAttribute( 'b' ) ).to.equal( 2 );
-				} );
-
-				it( 'should change the attribute of element', () => {
-					batch.setAttribute( 'a', 2, node );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( node.getAttribute( 'a' ) ).to.equal( 2 );
-				} );
-
-				it( 'should create the attribute on text node', () => {
-					batch.setAttribute( 'b', 2, text );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( root.getChild( 1 ).getAttribute( 'b' ) ).to.equal( 2 );
-				} );
-
-				it( 'should change the attribute of text node', () => {
-					batch.setAttribute( 'a', 2, text );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.equal( 2 );
-				} );
-
-				it( 'should do nothing if the attribute value is the same', () => {
-					batch.setAttribute( 'a', 1, node );
-					expect( spy.callCount ).to.equal( 0 );
-					expect( node.getAttribute( 'a' ) ).to.equal( 1 );
-				} );
-			} );
-
-			describe( 'removeAttribute', () => {
-				it( 'should remove the attribute from element', () => {
-					batch.removeAttribute( 'a', node );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( node.getAttribute( 'a' ) ).to.be.undefined;
-				} );
-
-				it( 'should remove the attribute from character', () => {
-					batch.removeAttribute( 'a', text );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.be.undefined;
-				} );
-
-				it( 'should do nothing if the attribute is not set', () => {
-					batch.removeAttribute( 'b', node );
-					expect( spy.callCount ).to.equal( 0 );
-				} );
-			} );
-		} );
-
-		describe( 'change attribute on range', () => {
-			beforeEach( () => {
-				const element = batch.createElement( 'e', { a: 2 } );
-
-				batch.appendText( 'xxx', { a: 1 }, root );
-				batch.appendText( 'xxx', root );
-				batch.appendText( 'xxx', { a: 1 }, root );
-				batch.appendText( 'xxx', { a: 2 }, root );
-				batch.appendText( 'xxx', root );
-				batch.appendText( 'xxx', { a: 1 }, root );
-				batch.appendText( 'xxx', element );
-				batch.append( element, root );
-				batch.appendText( 'xxx', root );
-
-				spy = sinon.spy( doc, 'applyOperation' );
-			} );
-
-			function getRange( startIndex, endIndex ) {
-				return new Range(
-					Position.createFromParentAndOffset( root, startIndex ),
-					Position.createFromParentAndOffset( root, endIndex )
-				);
-			}
-
-			function getChangesAttrsCount() {
-				let totalNumber = 0;
-
-				for ( const delta of batch.deltas ) {
-					for ( const operation of delta.operations ) {
-						if ( operation.range ) {
-							totalNumber += count( operation.range.getItems( { singleCharacters: true } ) );
-						}
-					}
-				}
-
-				return totalNumber;
-			}
-
-			function getCompressedAttrs() {
-				// default: 111---111222---1112------
-				const range = Range.createIn( root );
-
-				return Array.from( range.getItems( { singleCharacters: true } ) )
-					.map( item => item.getAttribute( 'a' ) || '-' )
-					.join( '' );
-			}
-
-			describe( 'setAttribute', () => {
-				it( 'should set the attribute on the range', () => {
-					batch.setAttribute( 'a', 3, getRange( 3, 6 ) );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( getChangesAttrsCount() ).to.equal( 3 );
-					expect( getCompressedAttrs() ).to.equal( '111333111222---1112------' );
-				} );
-
-				it( 'should split the operations if parts of the range have different attributes', () => {
-					batch.setAttribute( 'a', 3, getRange( 4, 14 ) );
-					expect( spy.callCount ).to.equal( 4 );
-					expect( getChangesAttrsCount() ).to.equal( 10 );
-					expect( getCompressedAttrs() ).to.equal( '111-3333333333-1112------' );
-				} );
-
-				it( 'should split the operations if parts of the part of the range have the attribute', () => {
-					batch.setAttribute( 'a', 2, getRange( 4, 14 ) );
-					expect( spy.callCount ).to.equal( 3 );
-					expect( getChangesAttrsCount() ).to.equal( 7 );
-					expect( getCompressedAttrs() ).to.equal( '111-2222222222-1112------' );
-				} );
-
-				it( 'should strip the range if the beginning have the attribute', () => {
-					batch.setAttribute( 'a', 1, getRange( 1, 5 ) );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( getChangesAttrsCount() ).to.equal( 2 );
-					expect( getCompressedAttrs() ).to.equal( '11111-111222---1112------' );
-				} );
-
-				it( 'should strip the range if the ending have the attribute', () => {
-					batch.setAttribute( 'a', 1, getRange( 13, 17 ) );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( getChangesAttrsCount() ).to.equal( 2 );
-					expect( getCompressedAttrs() ).to.equal( '111---111222-111112------' );
-				} );
-
-				it( 'should do nothing if the range has attribute', () => {
-					batch.setAttribute( 'a', 1, getRange( 0, 3 ) );
-					expect( spy.callCount ).to.equal( 0 );
-					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-				} );
-
-				it( 'should not check range\'s start position node when creating operations', () => {
-					const range = new Range(
-						new Position( root, [ 18, 1 ] ),
-						new Position( root, [ 19 ] )
-					);
-
-					batch.setAttribute( 'a', 1, range );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( getChangesAttrsCount() ).to.equal( 2 );
-					expect( getCompressedAttrs() ).to.equal( '111---111222---1112-11---' );
-				} );
-
-				it( 'should not change elements attribute if range contains closing tag', () => {
-					const range = new Range(
-						new Position( root, [ 18, 1 ] ),
-						new Position( root, [ 21 ] )
-					);
-
-					batch.setAttribute( 'a', 1, range );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( getChangesAttrsCount() ).to.equal( 4 );
-					expect( getCompressedAttrs() ).to.equal( '111---111222---1112-1111-' );
-				} );
-
-				it( 'should not create an operation if the range contains only closing tag', () => {
-					const range = new Range(
-						new Position( root, [ 18, 3 ] ),
-						new Position( root, [ 19 ] )
-					);
-
-					batch.setAttribute( 'a', 3, range );
-					expect( spy.callCount ).to.equal( 0 );
-					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-				} );
-
-				it( 'should not create an operation if is collapsed', () => {
-					batch.setAttribute( 'a', 1, getRange( 3, 3 ) );
-					expect( spy.callCount ).to.equal( 0 );
-					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-				} );
-
-				it( 'should create a proper operations for the mixed range', () => {
-					batch.setAttribute( 'a', 1, getRange( 0, 20 ) );
-					expect( spy.callCount ).to.equal( 5 );
-					expect( getChangesAttrsCount() ).to.equal( 14 );
-					expect( getCompressedAttrs() ).to.equal( '11111111111111111111111--' );
-				} );
-			} );
-
-			describe( 'removeAttribute', () => {
-				it( 'should remove the attribute on the range', () => {
-					batch.removeAttribute( 'a', getRange( 0, 2 ) );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( getChangesAttrsCount() ).to.equal( 2 );
-					expect( getCompressedAttrs() ).to.equal( '--1---111222---1112------' );
-				} );
-
-				it( 'should split the operations if parts of the range have different attributes', () => {
-					batch.removeAttribute( 'a', getRange( 7, 11 ) );
-					expect( spy.callCount ).to.equal( 2 );
-					expect( getChangesAttrsCount() ).to.equal( 4 );
-					expect( getCompressedAttrs() ).to.equal( '111---1----2---1112------' );
-				} );
-
-				it( 'should split the operations if parts of the part of the range have no attribute', () => {
-					batch.removeAttribute( 'a', getRange( 1, 7 ) );
-					expect( spy.callCount ).to.equal( 2 );
-					expect( getChangesAttrsCount() ).to.equal( 3 );
-					expect( getCompressedAttrs() ).to.equal( '1------11222---1112------' );
-				} );
-
-				it( 'should strip the range if the beginning have no attribute', () => {
-					batch.removeAttribute( 'a', getRange( 4, 12 ) );
-					expect( spy.callCount ).to.equal( 2 );
-					expect( getChangesAttrsCount() ).to.equal( 6 );
-					expect( getCompressedAttrs() ).to.equal( '111------------1112------' );
-				} );
-
-				it( 'should strip the range if the ending have no attribute', () => {
-					batch.removeAttribute( 'a', getRange( 7, 15 ) );
-					expect( spy.callCount ).to.equal( 2 );
-					expect( getChangesAttrsCount() ).to.equal( 5 );
-					expect( getCompressedAttrs() ).to.equal( '111---1--------1112------' );
-				} );
-
-				it( 'should do nothing if the range has no attribute', () => {
-					batch.removeAttribute( 'a', getRange( 4, 5 ) );
-					expect( spy.callCount ).to.equal( 0 );
-					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-				} );
-
-				it( 'should not check range\'s start position node when creating operations', () => {
-					const range = new Range(
-						new Position( root, [ 18, 3 ] ),
-						new Position( root, [ 19 ] )
-					);
-
-					batch.removeAttribute( 'a', range );
-					expect( spy.callCount ).to.equal( 0 );
-					expect( getChangesAttrsCount() ).to.equal( 0 );
-					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-				} );
-
-				it( 'should not apply operation twice in the range contains opening and closing tags', () => {
-					batch.removeAttribute( 'a', getRange( 18, 22 ) );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( getChangesAttrsCount() ).to.equal( 1 );
-					expect( getCompressedAttrs() ).to.equal( '111---111222---111-------' );
-				} );
-
-				it( 'should not create an operation if range is collapsed', () => {
-					batch.removeAttribute( 'a', getRange( 3, 3 ) );
-					expect( spy.callCount ).to.equal( 0 );
-					expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
-				} );
-
-				it( 'should create a proper operations for the mixed range', () => {
-					batch.removeAttribute( 'a', getRange( 3, 15 ) );
-					expect( spy.callCount ).to.equal( 2 );
-					expect( getChangesAttrsCount() ).to.equal( 6 );
-					expect( getCompressedAttrs() ).to.equal( '111------------1112------' );
-				} );
-			} );
-		} );
-
-		describe( 'change attribute on root element', () => {
-			let p;
-
-			beforeEach( () => {
-				p = batch.createElement( 'p', { a: 3 } );
-				spy = sinon.spy( doc, 'applyOperation' );
-			} );
-
-			describe( 'setAttribute', () => {
-				it( 'should create the attribute on root', () => {
-					batch.setAttribute( 'b', 2, root );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( root.getAttribute( 'b' ) ).to.equal( 2 );
-				} );
-
-				it( 'should create the attribute on detached root', () => {
-					batch.setAttribute( 'b', 2, p );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( p.getAttribute( 'b' ) ).to.equal( 2 );
-				} );
-
-				it( 'should change the attribute of root', () => {
-					batch.setAttribute( 'a', 2, root );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( root.getAttribute( 'a' ) ).to.equal( 2 );
-				} );
-
-				it( 'should change the attribute of detached root', () => {
-					batch.setAttribute( 'a', 2, p );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( p.getAttribute( 'a' ) ).to.equal( 2 );
-				} );
-
-				it( 'should do nothing if the attribute value is the same', () => {
-					batch.setAttribute( 'a', 1, root );
-					expect( spy.callCount ).to.equal( 1 );
-					batch.setAttribute( 'a', 1, root );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( root.getAttribute( 'a' ) ).to.equal( 1 );
-				} );
-
-				it( 'should do nothing if the attribute value is the same on detached root', () => {
-					batch.setAttribute( 'a', 1, p );
-					expect( spy.callCount ).to.equal( 1 );
-					batch.setAttribute( 'a', 1, p );
-					expect( spy.callCount ).to.equal( 1 );
-					expect( p.getAttribute( 'a' ) ).to.equal( 1 );
-				} );
-			} );
-
-			describe( 'removeAttribute', () => {
-				it( 'should remove the attribute from root', () => {
-					batch.setAttribute( 'a', 1, root );
-					batch.removeAttribute( 'a', root );
-
-					expect( spy.callCount ).to.equal( 2 );
-					expect( root.getAttribute( 'a' ) ).to.be.undefined;
-				} );
-
-				it( 'should do nothing if the attribute is not set', () => {
-					batch.removeAttribute( 'b', root );
-					expect( spy.callCount ).to.equal( 0 );
-				} );
-			} );
-
-			describe( 'clearAttributes', () => {
-				it( 'should clear attributes from range', () => {
-					batch.appendText( 'xxx', { a: 1, b: 2, c: 3 }, root );
-					batch.appendText( 'xxx', root );
-					batch.appendText( 'xxx', { a: 1 }, root );
-					batch.appendText( 'xxx', { b: 2 }, root );
-					batch.appendText( 'xxx', root );
-					batch.appendElement( 'e', { a: 1 }, root );
-					batch.appendText( 'xxx', root );
-
-					const range = Range.createIn( root );
-
-					batch.clearAttributes( range );
-
-					let itemsCount = 0;
-
-					for ( const item of range.getItems() ) {
-						itemsCount++;
-						expect( Array.from( item.getAttributeKeys() ).length ).to.equal( 0 );
-					}
-
-					expect( itemsCount ).to.equal( 3 );
-				} );
-
-				it( 'should clear attributes on element', () => {
-					const element = batch.createElement( 'x', { a: 1, b: 2, c: 3 }, root );
-
-					expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 3 );
-
-					batch.clearAttributes( element );
-
-					expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 0 );
-				} );
-
-				it( 'should clear attributes on root element', () => {
-					batch.setAttributes( { a: 1, b: 2, c: 3 }, root );
-
-					expect( Array.from( root.getAttributeKeys() ).length ).to.equal( 3 );
-
-					batch.clearAttributes( root );
-
-					expect( Array.from( root.getAttributeKeys() ).length ).to.equal( 0 );
-				} );
-
-				it( 'should do nothing if there are no attributes', () => {
-					const element = batch.createElement( 'x' );
-
-					expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 0 );
-
-					batch.clearAttributes( element );
-
-					expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 0 );
-				} );
-			} );
-		} );
-
-		it( 'should not add empty delta to the batch', () => {
-			const nodeA = new Element( 'p', { a: 1 } );
-			const nodeB = new Element( 'p', { b: 2 } );
-			root.insertChildren( 0, [ nodeA, nodeB ] );
-
-			batch.setAttribute( 'a', 1, nodeA );
-
-			expect( batch.deltas.length ).to.equal( 0 );
-
-			batch.removeAttribute( 'x', Range.createIn( root ) );
-
-			expect( batch.deltas.length ).to.equal( 0 );
-		} );
-	} );
-
-	describe( 'setAttributes()', () => {
-		let doc, batch, frag, item;
-
-		beforeEach( () => {
-			doc = new Document();
-			batch = doc.batch();
-
-			frag = batch.createDocumentFragment();
-			item = batch.createText( 'xxx', { b: 2, c: 3 } );
-
-			batch.appendText( 'xxx', { a: 1 }, frag );
-			batch.append( item, frag );
-		} );
-
-		it( 'should set attributes one by one on range', () => {
-			const range = Range.createIn( frag );
-
-			// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
-			// such a big amount of the same tests, so let's use a spy here.
-			const spy = sinon.spy( batch, 'setAttribute' );
-
-			batch.setAttributes( { a: 3, c: null }, range );
-
-			// Verify result.
-			expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] );
-			expect( Array.from( frag.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
-
-			// Verify operations
-			sinon.assert.calledTwice( spy );
-			sinon.assert.calledWith( spy.firstCall, 'a', 3, range );
-			sinon.assert.calledWith( spy.secondCall, 'c', null, range );
-		} );
-
-		it( 'should set attributes one by one on range for map as attributes list', () => {
-			const range = Range.createIn( frag );
-
-			// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
-			// such a big amount of the same tests, so let's use a spy here.
-			const spy = sinon.spy( batch, 'setAttribute' );
-
-			batch.setAttributes( new Map( [ [ 'a', 3 ], [ 'c', null ] ] ), range );
-
-			// Verify result.
-			expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] );
-			expect( Array.from( frag.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
-
-			// Verify operations
-			sinon.assert.calledTwice( spy );
-			sinon.assert.calledWith( spy.firstCall, 'a', 3, range );
-			sinon.assert.calledWith( spy.secondCall, 'c', null, range );
-		} );
-
-		it( 'should set attributes one by one on item', () => {
-			// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
-			// such a big amount of the same tests, so let's use a spy here.
-			const spy = sinon.spy( batch, 'setAttribute' );
-
-			batch.setAttributes( { a: 3, c: null }, item );
-
-			// Verify result.
-			expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
-
-			// Verify operations
-			sinon.assert.calledTwice( spy );
-			sinon.assert.calledWith( spy.firstCall, 'a', 3, item );
-			sinon.assert.calledWith( spy.secondCall, 'c', null, item );
-		} );
-
-		it( 'should set attributes one by one on item for maps as attributes list', () => {
-			// `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
-			// such a big amount of the same tests, so let's use a spy here.
-			const spy = sinon.spy( batch, 'setAttribute' );
-
-			batch.setAttributes( new Map( [ [ 'a', 3 ], [ 'c', null ] ] ), item );
-
-			// Verify result.
-			expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
-
-			// Verify operations
-			sinon.assert.calledTwice( spy );
-			sinon.assert.calledWith( spy.firstCall, 'a', 3, item );
-			sinon.assert.calledWith( spy.secondCall, 'c', null, item );
-		} );
-	} );
-
-	describe( 'merge()', () => {
-		let doc, root, p1, p2;
-
-		beforeEach( () => {
-			doc = new Document();
-			root = doc.createRoot();
-
-			p1 = new Element( 'p', { key1: 'value1' }, new Text( 'foo' ) );
-			p2 = new Element( 'p', { key2: 'value2' }, new Text( 'bar' ) );
-
-			root.insertChildren( 0, [ p1, p2 ] );
-		} );
-
-		it( 'should merge foo and bar into foobar', () => {
-			doc.batch().merge( new Position( root, [ 1 ] ) );
-
-			expect( root.maxOffset ).to.equal( 1 );
-			expect( root.getChild( 0 ).name ).to.equal( 'p' );
-			expect( root.getChild( 0 ).maxOffset ).to.equal( 6 );
-			expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
-			expect( root.getChild( 0 ).getAttribute( 'key1' ) ).to.equal( 'value1' );
-			expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' );
-		} );
-
-		it( 'should throw if there is no element after', () => {
-			expect( () => {
-				doc.batch().merge( new Position( root, [ 2 ] ) );
-			} ).to.throw( CKEditorError, /^batch-merge-no-element-after/ );
-		} );
-
-		it( 'should throw if there is no element before', () => {
-			expect( () => {
-				doc.batch().merge( new Position( root, [ 0, 2 ] ) );
-			} ).to.throw( CKEditorError, /^batch-merge-no-element-before/ );
-		} );
-	} );
-
-	describe( 'move()', () => {
-		let doc, root, range, div, p, batch;
-
-		beforeEach( () => {
-			doc = new Document();
-			root = doc.createRoot();
-
-			div = new Element( 'div', [], new Text( 'foobar' ) );
-			p = new Element( 'p', [], new Text( 'abcxyz' ) );
-
-			div.insertChildren( 0, [ new Element( 'p', [], new Text( 'gggg' ) ) ] );
-			div.insertChildren( 2, [ new Element( 'p', [], new Text( 'hhhh' ) ) ] );
-
-			root.insertChildren( 0, [ div, p ] );
-
-			range = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 7 ] ) );
-
-			batch = doc.batch();
-		} );
-
-		it( 'should move flat range of nodes', () => {
-			batch.move( range, new Position( root, [ 1, 3 ] ) );
-
-			expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggggPfoPhhhhP' );
-			expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcobarxyz' );
-		} );
-
-		it( 'should throw if object to move is not a range', () => {
-			expect( () => {
-				doc.batch().move( root.getChild( 0 ), new Position( root, [ 1, 3 ] ) );
-			} ).to.throw( CKEditorError, /^batch-move-invalid-range/ );
-		} );
-
-		it( 'should throw if given range is not flat', () => {
-			const notFlatRange = new Range( new Position( root, [ 0, 2, 2 ] ), new Position( root, [ 0, 6 ] ) );
-
-			expect( () => {
-				doc.batch().move( notFlatRange, new Position( root, [ 1, 3 ] ) );
-			} ).to.throw( CKEditorError, /^batch-move-range-not-flat/ );
-		} );
-
-		it( 'should throw if range is going to be moved to the other document', () => {
-			const docFrag = batch.createDocumentFragment();
-
-			expect( () => {
-				doc.batch().move( range, docFrag );
-			} ).to.throw( CKEditorError, /^batch-move-different-document/ );
-		} );
-	} );
-
-	describe( 'remove()', () => {
-		let doc, batch, div, p, range;
-
-		beforeEach( () => {
-			doc = new Document();
-			batch = doc.batch();
-
-			div = batch.createElement( 'div' );
-			batch.appendText( 'foobar', div );
-
-			p = batch.createElement( 'p' );
-			batch.appendText( 'abcxyz', p );
-
-			batch.insertElement( 'p', div );
-			batch.appendElement( 'p', div );
-
-			batch.insertText( 'gggg', new Position( div, [ 0, 0 ] ) );
-			batch.insertText( 'hhhh', new Position( div, [ 7, 0 ] ) );
-		} );
-
-		describe( 'remove from document', () => {
-			let root;
-
-			beforeEach( () => {
-				root = doc.createRoot();
-				batch.append( div, root );
-				batch.append( p, root );
-
-				// Reset batch.
-				batch = doc.batch();
-
-				// Range starts in ROOT > DIV > P > gg|gg.
-				// Range ends in ROOT > DIV > ...|ar.
-				range = new Range( new Position( root, [ 0, 0, 2 ] ), new Position( root, [ 0, 5 ] ) );
-			} );
-
-			it( 'should remove specified node', () => {
-				batch.remove( div );
-
-				expect( root.maxOffset ).to.equal( 1 );
-				expect( root.childCount ).to.equal( 1 );
-				expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' );
-			} );
-
-			it( 'should remove specified text node', () => {
-				batch.remove( p.getChild( 0 ) );
-
-				expect( getNodesAndText( Range.createOn( p ) ) ).to.equal( 'PP' );
-			} );
-
-			it( 'should remove any range of nodes', () => {
-				batch.remove( range );
-
-				expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggParPhhhhP' );
-				expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcxyz' );
-			} );
-
-			it( 'should create minimal number of remove deltas, each with only one operation', () => {
-				batch.remove( range );
-
-				expect( batch.deltas.length ).to.equal( 2 );
-				expect( batch.deltas[ 0 ].operations.length ).to.equal( 1 );
-				expect( batch.deltas[ 1 ].operations.length ).to.equal( 1 );
-			} );
-
-			it( 'should use RemoveOperation', () => {
-				batch.remove( div );
-
-				expect( batch.deltas[ 0 ].operations[ 0 ].type ).to.equal( 'remove' );
-			} );
-		} );
-
-		describe( 'remove from document fragment', () => {
-			let frag;
-
-			beforeEach( () => {
-				frag = batch.createDocumentFragment();
-				batch.append( div, frag );
-				batch.append( p, frag );
-
-				// Reset batch.
-				batch = doc.batch();
-
-				// Range starts in FRAG > DIV > P > gg|gg.
-				// Range ends in FRAG > DIV > ...|ar.
-				range = new Range( new Position( frag, [ 0, 0, 2 ] ), new Position( frag, [ 0, 5 ] ) );
-			} );
-
-			it( 'should remove specified node', () => {
-				batch.remove( div );
-
-				expect( frag.maxOffset ).to.equal( 1 );
-				expect( frag.childCount ).to.equal( 1 );
-				expect( getNodesAndText( Range.createIn( frag.getChild( 0 ) ) ) ).to.equal( 'abcxyz' );
-			} );
-
-			it( 'should remove specified text node', () => {
-				batch.remove( p.getChild( 0 ) );
-
-				expect( getNodesAndText( Range.createOn( p ) ) ).to.equal( 'PP' );
-			} );
-
-			it( 'should remove any range of nodes', () => {
-				batch.remove( range );
-
-				expect( getNodesAndText( Range.createIn( frag.getChild( 0 ) ) ) ).to.equal( 'PggParPhhhhP' );
-				expect( getNodesAndText( Range.createIn( frag.getChild( 1 ) ) ) ).to.equal( 'abcxyz' );
-			} );
-
-			it( 'should create minimal number of remove deltas, each with only one operation', () => {
-				batch.remove( range );
-
-				expect( batch.deltas.length ).to.equal( 2 );
-				expect( batch.deltas[ 0 ].operations.length ).to.equal( 1 );
-				expect( batch.deltas[ 1 ].operations.length ).to.equal( 1 );
-			} );
-
-			it( 'should use DetachOperation', () => {
-				batch.remove( div );
-
-				expect( batch.deltas[ 0 ].operations[ 0 ].type ).to.equal( 'detach' );
-			} );
-		} );
-	} );
-
-	describe( 'rename()', () => {
-		let doc, root, batch;
-
-		beforeEach( () => {
-			doc = new Document();
-			root = doc.createRoot();
-
-			const p = new Element( 'p', null, new Text( 'abc' ) );
-			root.appendChildren( p );
-
-			batch = doc.batch();
-
-			batch.rename( p, 'h' );
-		} );
-
-		it( 'should rename given element', () => {
-			expect( root.maxOffset ).to.equal( 1 );
-			expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' );
-		} );
-
-		it( 'should throw if not an Element instance is passed', () => {
-			expect( () => {
-				batch.rename( new Text( 'abc' ), 'h' );
-			} ).to.throw( CKEditorError, /^batch-rename-not-element-instance/ );
-		} );
-	} );
-
-	describe( 'split()', () => {
-		let doc, root, p;
-
-		beforeEach( () => {
-			doc = new Document();
-			root = doc.createRoot();
-
-			p = new Element( 'p', { key: 'value' }, new Text( 'foobar' ) );
-
-			root.insertChildren( 0, p );
-		} );
-
-		it( 'should split foobar to foo and bar', () => {
-			doc.batch().split( new Position( root, [ 0, 3 ] ) );
-
-			expect( root.maxOffset ).to.equal( 2 );
-
-			expect( root.getChild( 0 ).name ).to.equal( 'p' );
-			expect( root.getChild( 0 ).maxOffset ).to.equal( 3 );
-			expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
-			expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
-
-			expect( root.getChild( 1 ).name ).to.equal( 'p' );
-			expect( root.getChild( 1 ).maxOffset ).to.equal( 3 );
-			expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 );
-			expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'bar' );
-		} );
-
-		it( 'should create an empty paragraph if we split at the end', () => {
-			doc.batch().split( new Position( root, [ 0, 6 ] ) );
-
-			expect( root.maxOffset ).to.equal( 2 );
-
-			expect( root.getChild( 0 ).name ).to.equal( 'p' );
-			expect( root.getChild( 0 ).maxOffset ).to.equal( 6 );
-			expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
-			expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
-			expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' );
-
-			expect( root.getChild( 1 ).name ).to.equal( 'p' );
-			expect( root.getChild( 1 ).maxOffset ).to.equal( 0 );
-			expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 );
-			expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
-		} );
-
-		it( 'should throw if we try to split a root', () => {
-			expect( () => {
-				doc.batch().split( new Position( root, [ 0 ] ) );
-			} ).to.throw( CKEditorError, /^batch-split-element-no-parent/ );
-		} );
-
-		it( 'should throw if we try to split an element with no parent', () => {
-			const batch = doc.batch();
-
-			expect( () => {
-				const element = batch.createElement( 'p' );
-
-				batch.split( new Position( element, [ 0 ] ) );
-			} ).to.throw( CKEditorError, /^batch-split-element-no-parent/ );
-		} );
-
-		it( 'should throw if we try to split a document fragment', () => {
-			const batch = doc.batch();
-
-			expect( () => {
-				const documentFragment = batch.createDocumentFragment();
-
-				batch.split( new Position( documentFragment, [ 0 ] ) );
-			} ).to.throw( CKEditorError, /^batch-split-element-no-parent/ );
-		} );
-	} );
-
-	describe( 'wrap()', () => {
-		let doc, root, range;
-
-		beforeEach( () => {
-			doc = new Document();
-			root = doc.createRoot();
-
-			root.insertChildren( 0, new Text( 'foobar' ) );
-
-			range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) );
-		} );
-
-		it( 'should wrap flat range with given element', () => {
-			const p = new Element( 'p' );
-			doc.batch().wrap( range, p );
-
-			expect( root.maxOffset ).to.equal( 5 );
-			expect( root.getChild( 0 ).data ).to.equal( 'fo' );
-			expect( root.getChild( 1 ) ).to.equal( p );
-			expect( p.getChild( 0 ).data ).to.equal( 'ob' );
-			expect( root.getChild( 2 ).data ).to.equal( 'ar' );
-		} );
-
-		it( 'should wrap flat range with an element of given name', () => {
-			doc.batch().wrap( range, 'p' );
-
-			expect( root.maxOffset ).to.equal( 5 );
-			expect( root.getChild( 0 ).data ).to.equal( 'fo' );
-			expect( root.getChild( 1 ).name ).to.equal( 'p' );
-			expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'ob' );
-			expect( root.getChild( 2 ).data ).to.equal( 'ar' );
-		} );
-
-		it( 'should throw if range to wrap is not flat', () => {
-			root.insertChildren( 1, [ new Element( 'p', [], new Text( 'xyz' ) ) ] );
-			const notFlatRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 6, 2 ] ) );
-
-			expect( () => {
-				doc.batch().wrap( notFlatRange, 'p' );
-			} ).to.throw( CKEditorError, /^batch-wrap-range-not-flat/ );
-		} );
-
-		it( 'should throw if element to wrap with has children #1', () => {
-			const p = new Element( 'p', [], new Text( 'a' ) );
-
-			expect( () => {
-				doc.batch().wrap( range, p );
-			} ).to.throw( CKEditorError, /^batch-wrap-element-not-empty/ );
-		} );
-
-		it( 'should throw if element to wrap with has children #2', () => {
-			const p = new Element( 'p' );
-			root.insertChildren( 0, p );
-
-			expect( () => {
-				doc.batch().wrap( range, p );
-			} ).to.throw( CKEditorError, /^batch-wrap-element-attached/ );
-		} );
-	} );
-
-	describe( 'unwrap()', () => {
-		let doc, root, p;
-
-		beforeEach( () => {
-			doc = new Document();
-			root = doc.createRoot();
-
-			p = new Element( 'p', [], new Text( 'xyz' ) );
-			root.insertChildren( 0, [ new Text( 'a' ), p, new Text( 'b' ) ] );
-		} );
-
-		it( 'should unwrap given element', () => {
-			doc.batch().unwrap( p );
-
-			expect( root.maxOffset ).to.equal( 5 );
-			expect( root.getChild( 0 ).data ).to.equal( 'axyzb' );
-		} );
-
-		it( 'should throw if element to unwrap has no parent', () => {
-			const element = new Element( 'p' );
-
-			expect( () => {
-				doc.batch().unwrap( element );
-			} ).to.throw( CKEditorError, /^batch-unwrap-element-no-parent/ );
-		} );
-	} );
-
-	describe( 'setMarker()', () => {
-		let doc, root, range;
-
-		beforeEach( () => {
-			doc = new Document();
-			root = doc.createRoot();
-			root.appendChildren( new Text( 'foo' ) );
-			range = Range.createIn( root );
-		} );
-
-		it( 'should add marker to the document marker collection', () => {
-			doc.batch().setMarker( 'name', range );
-
-			expect( doc.markers.get( 'name' ).getRange().isEqual( range ) ).to.be.true;
-		} );
-
-		it( 'should update marker in the document marker collection', () => {
-			doc.batch().setMarker( 'name', range );
-
-			const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 );
-			doc.batch().setMarker( 'name', range2 );
-
-			expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true;
-		} );
-
-		it( 'should accept marker instance', () => {
-			doc.batch().setMarker( 'name', range );
-			const marker = doc.markers.get( 'name' );
-			const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 );
-
-			const batch = doc.batch();
-			batch.setMarker( marker, range2 );
-			const op = batch.deltas[ 0 ].operations[ 0 ];
-
-			expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true;
-			expect( op.oldRange.isEqual( range ) ).to.be.true;
-			expect( op.newRange.isEqual( range2 ) ).to.be.true;
-		} );
-
-		it( 'should accept empty range parameter if marker instance is passed', () => {
-			const marker = doc.markers.set( 'name', range );
-
-			sinon.spy( doc, 'fire' );
-
-			doc.on( 'change', ( evt, type, changes ) => {
-				if ( type == 'marker' ) {
-					expect( changes.type ).to.equal( 'set' );
-					expect( changes.name ).to.equal( 'name' );
-				}
-			} );
-
-			const batch = doc.batch();
-			batch.setMarker( marker );
-			const op = batch.deltas[ 0 ].operations[ 0 ];
-
-			expect( doc.fire.calledWith( 'change', 'marker' ) ).to.be.true;
-			expect( op.oldRange ).to.be.null;
-			expect( op.newRange.isEqual( range ) ).to.be.true;
-		} );
-
-		it( 'should throw if marker with given name does not exist and range is not passed', () => {
-			expect( () => {
-				doc.batch().setMarker( 'name' );
-			} ).to.throw( CKEditorError, /^batch-setMarker-no-range/ );
-		} );
-	} );
-
-	describe( 'removeMarker()', () => {
-		let doc, root, range;
-
-		beforeEach( () => {
-			doc = new Document();
-			root = doc.createRoot();
-			root.appendChildren( new Text( 'foo' ) );
-			range = Range.createIn( root );
-		} );
-
-		it( 'should remove marker from the document marker collection', () => {
-			doc.batch().setMarker( 'name', range );
-			doc.batch().removeMarker( 'name' );
-
-			expect( doc.markers.get( 'name' ) ).to.be.null;
-		} );
-
-		it( 'should throw when trying to remove non existing marker', () => {
-			expect( () => {
-				doc.batch().removeMarker( 'name' );
-			} ).to.throw( CKEditorError, /^batch-removeMarker-no-marker/ );
-		} );
-
-		it( 'should accept marker instance', () => {
-			doc.batch().setMarker( 'name', range );
-			const marker = doc.markers.get( 'name' );
-
-			doc.batch().removeMarker( marker );
-
-			expect( doc.markers.get( 'name' ) ).to.be.null;
-		} );
-	} );
 } );

+ 5 - 4
packages/ckeditor5-engine/tests/model/delta/attributedelta.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Range from '../../../src/model/range';
 import Position from '../../../src/model/position';
 import AttributeDelta from '../../../src/model/delta/attributedelta';
@@ -12,11 +12,12 @@ import NoOperation from '../../../src/model/operation/nooperation';
 import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 
 describe( 'AttributeDelta', () => {
-	let doc, root, delta;
+	let root, delta;
 
 	beforeEach( () => {
-		doc = new Document();
-		root = doc.createRoot();
+		const model = new Model();
+
+		root = model.document.createRoot();
 		delta = new AttributeDelta();
 	} );
 

+ 4 - 3
packages/ckeditor5-engine/tests/model/delta/delta.js

@@ -14,7 +14,7 @@ import ReinsertOperation from '../../../src/model/operation/reinsertoperation';
 import RemoveOperation from '../../../src/model/operation/removeoperation';
 import RootAttributeOperation from '../../../src/model/operation/rootattributeoperation';
 import DeltaFactory from '../../../src/model/delta/deltafactory';
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Position from '../../../src/model/position';
 import Range from '../../../src/model/range';
 import { jsonParseStringify } from '../../../tests/model/_utils/utils';
@@ -148,9 +148,10 @@ describe( 'Delta', () => {
 		} );
 
 		beforeEach( () => {
-			delta = new FooDelta();
+			const model = new Model();
 
-			doc = new Document();
+			delta = new FooDelta();
+			doc = model.document;
 			root = doc.createRoot();
 		} );
 

+ 5 - 4
packages/ckeditor5-engine/tests/model/delta/deltafactory.js

@@ -18,7 +18,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import DeltaFactory from '../../../src/model/delta/deltafactory';
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Position from '../../../src/model/position';
 import Range from '../../../src/model/range';
 import { jsonParseStringify } from '../../../tests/model/_utils/utils';
@@ -44,15 +44,16 @@ describe( 'DeltaFactory', () => {
 		} );
 
 		beforeEach( () => {
-			delta = new FooDelta();
+			const model = new Model();
 
-			doc = new Document();
+			delta = new FooDelta();
+			doc = model.document;
 			root = doc.createRoot();
 		} );
 
 		it( 'should throw error for unregistered delta', () => {
 			expect( () => {
-				DeltaFactory.fromJSON( jsonParseStringify( new BarDelta() ), {} );
+				DeltaFactory.fromJSON( jsonParseStringify( new BarDelta() ), doc );
 			} ).to.throw( CKEditorError, /^delta-fromjson-no-deserializer/ );
 		} );
 

+ 4 - 2
packages/ckeditor5-engine/tests/model/delta/insertdelta.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Element from '../../../src/model/element';
 import Position from '../../../src/model/position';
 
@@ -17,7 +17,9 @@ describe( 'InsertDelta', () => {
 	let insertDelta, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		const model = new Model();
+
+		doc = model.document;
 		root = doc.createRoot();
 		insertDelta = new InsertDelta();
 	} );

+ 4 - 2
packages/ckeditor5-engine/tests/model/delta/markerdelta.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Range from '../../../src/model/range';
 
 import MarkerDelta from '../../../src/model/delta/markerdelta';
@@ -13,7 +13,9 @@ describe( 'MarkerDelta', () => {
 	let markerDelta, doc, root, range;
 
 	beforeEach( () => {
-		doc = new Document();
+		const model = new Model();
+
+		doc = model.document;
 		root = doc.createRoot();
 		range = Range.createIn( root );
 		markerDelta = new MarkerDelta();

+ 4 - 2
packages/ckeditor5-engine/tests/model/delta/mergedelta.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Position from '../../../src/model/position';
 
 import MergeDelta from '../../../src/model/delta/mergedelta';
@@ -17,7 +17,9 @@ describe( 'MergeDelta', () => {
 	let mergeDelta, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		const model = new Model();
+
+		doc = model.document;
 		root = doc.createRoot();
 		mergeDelta = new MergeDelta();
 	} );

+ 4 - 2
packages/ckeditor5-engine/tests/model/delta/movedelta.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Position from '../../../src/model/position';
 
 import MoveDelta from '../../../src/model/delta/movedelta';
@@ -13,7 +13,9 @@ describe( 'MoveDelta', () => {
 	let moveDelta, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		const model = new Model();
+
+		doc = model.document;
 		root = doc.createRoot();
 		moveDelta = new MoveDelta();
 	} );

+ 12 - 11
packages/ckeditor5-engine/tests/model/delta/renamedelta.js

@@ -3,17 +3,18 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
 
 import RenameDelta from '../../../src/model/delta/renamedelta';
 
 describe( 'RenameDelta', () => {
-	let renameDelta, doc, root;
+	let renameDelta, model, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 		root = doc.createRoot();
 		renameDelta = new RenameDelta();
 	} );
@@ -40,18 +41,18 @@ describe( 'RenameDelta', () => {
 		it( 'should return correct RenameDelta', () => {
 			root.appendChildren( new Element( 'p', null, new Text( 'abc' ) ) );
 
-			const batch = doc.batch();
+			model.change( writer => {
+				writer.rename( root.getChild( 0 ), 'h' );
 
-			batch.rename( root.getChild( 0 ), 'h' );
+				const reversed = writer.batch.deltas[ 0 ].getReversed();
 
-			const reversed = batch.deltas[ 0 ].getReversed();
+				reversed.operations.forEach( operation => {
+					model.applyOperation( operation );
+				} );
 
-			reversed.operations.forEach( operation => {
-				doc.applyOperation( operation );
+				expect( root.maxOffset ).to.equal( 1 );
+				expect( root.getChild( 0 ) ).to.have.property( 'name', 'p' );
 			} );
-
-			expect( root.maxOffset ).to.equal( 1 );
-			expect( root.getChild( 0 ) ).to.have.property( 'name', 'p' );
 		} );
 	} );
 

+ 4 - 2
packages/ckeditor5-engine/tests/model/delta/splitdelta.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Position from '../../../src/model/position';
 import Element from '../../../src/model/element';
 
@@ -19,7 +19,9 @@ describe( 'SplitDelta', () => {
 	let splitDelta, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		const model = new Model();
+
+		doc = model.document;
 		root = doc.createRoot();
 		splitDelta = new SplitDelta();
 	} );

+ 4 - 3
packages/ckeditor5-engine/tests/model/delta/transform/_utils/utils.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../../../src/model/document';
+import Model from '../../../../../src/model/model';
 import Element from '../../../../../src/model/element';
 import Text from '../../../../../src/model/text';
 
@@ -216,12 +216,13 @@ export function expectOperation( op, params ) {
 
 export function applyDelta( delta, document ) {
 	for ( const op of delta.operations ) {
-		document.applyOperation( op );
+		document.model.applyOperation( op );
 	}
 }
 
 export function getFilledDocument() {
-	const doc = new Document();
+	const model = new Model();
+	const doc = model.document;
 	const root = doc.createRoot();
 
 	root.insertChildren( 0, [

+ 4 - 2
packages/ckeditor5-engine/tests/model/delta/transform/transform.js

@@ -7,7 +7,7 @@ import transformations from '../../../../src/model/delta/basic-transformations';
 import deltaTransform from '../../../../src/model/delta/transform';
 const transformDeltaSets = deltaTransform.transformDeltaSets;
 
-import Document from '../../../../src/model/document';
+import Model from '../../../../src/model/model';
 import Element from '../../../../src/model/element';
 import Text from '../../../../src/model/text';
 import Position from '../../../../src/model/position';
@@ -39,7 +39,9 @@ describe( 'transform', () => {
 	let doc, root, baseVersion;
 
 	beforeEach( () => {
-		doc = new Document();
+		const model = new Model();
+
+		doc = model.document;
 		root = doc.createRoot();
 
 		root.appendChildren( new Element( 'p', null, new Text( 'foobar' ) ) );

+ 4 - 2
packages/ckeditor5-engine/tests/model/delta/unwrapdelta.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Position from '../../../src/model/position';
 
 import UnwrapDelta from '../../../src/model/delta/unwrapdelta';
@@ -17,7 +17,9 @@ describe( 'UnwrapDelta', () => {
 	let unwrapDelta, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		const model = new Model();
+
+		doc = model.document;
 		root = doc.createRoot();
 		unwrapDelta = new UnwrapDelta();
 	} );

+ 4 - 2
packages/ckeditor5-engine/tests/model/delta/wrapdelta.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Position from '../../../src/model/position';
 import Element from '../../../src/model/element';
 
@@ -18,7 +18,9 @@ describe( 'WrapDelta', () => {
 	let wrapDelta, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		const model = new Model();
+
+		doc = model.document;
 		root = doc.createRoot();
 		wrapDelta = new WrapDelta();
 	} );

+ 13 - 11
packages/ckeditor5-engine/tests/model/document/change-event.js

@@ -3,6 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
+import Model from '../../../src/model/model';
 import Document from '../../../src/model/document';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
@@ -15,10 +16,11 @@ import RemoveOperation from '../../../src/model/operation/removeoperation';
 import { wrapInDelta } from '../../../tests/model/_utils/utils';
 
 describe( 'Document change event', () => {
-	let doc, root, graveyard, types, changes;
+	let model, doc, root, graveyard, types, changes;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = new Document( model );
 		root = doc.createRoot();
 		graveyard = doc.graveyard;
 
@@ -32,7 +34,7 @@ describe( 'Document change event', () => {
 	} );
 
 	it( 'should be fired when text is inserted', () => {
-		doc.applyOperation( wrapInDelta( new InsertOperation( new Position( root, [ 0 ] ), 'foo', doc.version ) ) );
+		model.applyOperation( wrapInDelta( new InsertOperation( new Position( root, [ 0 ] ), 'foo', doc.version ) ) );
 
 		expect( changes ).to.have.length( 1 );
 		expect( types[ 0 ] ).to.equal( 'insert' );
@@ -41,7 +43,7 @@ describe( 'Document change event', () => {
 
 	it( 'should be fired when element is inserted', () => {
 		const element = new Element( 'p' );
-		doc.applyOperation( wrapInDelta( new InsertOperation( new Position( root, [ 0 ] ), element, doc.version ) ) );
+		model.applyOperation( wrapInDelta( new InsertOperation( new Position( root, [ 0 ] ), element, doc.version ) ) );
 
 		expect( changes ).to.have.length( 1 );
 		expect( types[ 0 ] ).to.equal( 'insert' );
@@ -50,7 +52,7 @@ describe( 'Document change event', () => {
 
 	it( 'should be fired when nodes are inserted', () => {
 		const element = new Element( 'p' );
-		doc.applyOperation( wrapInDelta( new InsertOperation( new Position( root, [ 0 ] ), [ element, 'foo' ], doc.version ) ) );
+		model.applyOperation( wrapInDelta( new InsertOperation( new Position( root, [ 0 ] ), [ element, 'foo' ], doc.version ) ) );
 
 		expect( changes ).to.have.length( 1 );
 		expect( types[ 0 ] ).to.equal( 'insert' );
@@ -65,7 +67,7 @@ describe( 'Document change event', () => {
 
 		root.insertChildren( 0, [ p1, p2 ] );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new MoveOperation(
 				new Position( root, [ 0, 0 ] ),
 				3,
@@ -84,10 +86,10 @@ describe( 'Document change event', () => {
 		root.insertChildren( 0, new Text( 'foo' ) );
 
 		const removeOperation = new RemoveOperation( new Position( root, [ 0 ] ), 3, new Position( doc.graveyard, [ 0 ] ), doc.version );
-		doc.applyOperation( wrapInDelta( removeOperation ) );
+		model.applyOperation( wrapInDelta( removeOperation ) );
 
 		const reinsertOperation = removeOperation.getReversed();
-		doc.applyOperation( wrapInDelta( reinsertOperation ) );
+		model.applyOperation( wrapInDelta( reinsertOperation ) );
 
 		expect( changes ).to.have.length( 2 );
 
@@ -103,7 +105,7 @@ describe( 'Document change event', () => {
 	it( 'should be fired when attribute is inserted', () => {
 		root.insertChildren( 0, new Text( 'foo' ) );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new AttributeOperation(
 				Range.createFromParentsAndOffsets( root, 0, root, 3 ),
 				'key',
@@ -125,7 +127,7 @@ describe( 'Document change event', () => {
 		const elem = new Element( 'p', { key: 'old' } );
 		root.insertChildren( 0, elem );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new AttributeOperation(
 				Range.createFromParentsAndOffsets( root, 0, elem, 0 ),
 				'key',
@@ -147,7 +149,7 @@ describe( 'Document change event', () => {
 		const elem = new Element( 'p', { key: 'old' } );
 		root.insertChildren( 0, elem );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new AttributeOperation(
 				Range.createFromParentsAndOffsets( root, 0, elem, 0 ),
 				'key',

+ 85 - 146
packages/ckeditor5-engine/tests/model/document/document.js

@@ -3,8 +3,8 @@
  * For licensing, see LICENSE.md.
  */
 
+import Model from '../../../src/model/model';
 import Document from '../../../src/model/document';
-import Schema from '../../../src/model/schema';
 import RootElement from '../../../src/model/rootelement';
 import Batch from '../../../src/model/batch';
 import Delta from '../../../src/model/delta/delta';
@@ -17,99 +17,31 @@ import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 import { setData, getData } from '../../../src/dev-utils/model';
 
 describe( 'Document', () => {
-	let doc;
+	let model, doc;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = new Document( model );
+
+		// Normally Model is the one who creates Document instance and keeps it as reference.
+		// We have to be sure that Model uses the right Document instance.
+		model.document = doc;
 	} );
 
 	describe( 'constructor()', () => {
 		it( 'should create Document with no data, empty graveyard and selection set to default range', () => {
+			const doc = new Document( model );
+
+			expect( doc ).to.have.property( 'model' ).to.equal( model );
 			expect( doc ).to.have.property( 'roots' ).that.is.instanceof( Map );
 			expect( doc.roots.size ).to.equal( 1 );
 			expect( doc.graveyard ).to.be.instanceof( RootElement );
 			expect( doc.graveyard.maxOffset ).to.equal( 0 );
 			expect( count( doc.selection.getRanges() ) ).to.equal( 1 );
-
-			expect( doc.schema ).to.be.instanceof( Schema );
-		} );
-	} );
-
-	describe( 'getRootNames()', () => {
-		it( 'should return empty iterator if no roots exist', () => {
-			expect( count( doc.getRootNames() ) ).to.equal( 0 );
-		} );
-
-		it( 'should return an iterator of all roots without the graveyard', () => {
-			doc.createRoot( '$root', 'a' );
-			doc.createRoot( '$root', 'b' );
-
-			expect( Array.from( doc.getRootNames() ) ).to.deep.equal( [ 'a', 'b' ] );
-		} );
-	} );
-
-	describe( 'createRoot()', () => {
-		it( 'should create a new RootElement with default element and root names, add it to roots map and return it', () => {
-			const root = doc.createRoot();
-
-			expect( doc.roots.size ).to.equal( 2 );
-			expect( root ).to.be.instanceof( RootElement );
-			expect( root.maxOffset ).to.equal( 0 );
-			expect( root ).to.have.property( 'name', '$root' );
-			expect( root ).to.have.property( 'rootName', 'main' );
-		} );
-
-		it( 'should create a new RootElement with custom element and root names, add it to roots map and return it', () => {
-			const root = doc.createRoot( 'customElementName', 'customRootName' );
-
-			expect( doc.roots.size ).to.equal( 2 );
-			expect( root ).to.be.instanceof( RootElement );
-			expect( root.maxOffset ).to.equal( 0 );
-			expect( root ).to.have.property( 'name', 'customElementName' );
-			expect( root ).to.have.property( 'rootName', 'customRootName' );
-		} );
-
-		it( 'should throw an error when trying to create a second root with the same name', () => {
-			doc.createRoot( '$root', 'rootName' );
-
-			expect(
-				() => {
-					doc.createRoot( '$root', 'rootName' );
-				}
-			).to.throw( CKEditorError, /model-document-createRoot-name-exists/ );
-		} );
-	} );
-
-	describe( 'getRoot()', () => {
-		it( 'should return a RootElement previously created with given name', () => {
-			const newRoot = doc.createRoot();
-			const getRoot = doc.getRoot();
-
-			expect( getRoot ).to.equal( newRoot );
-		} );
-
-		it( 'should throw an error when trying to get non-existent root', () => {
-			expect(
-				() => {
-					doc.getRoot( 'root' );
-				}
-			).to.throw( CKEditorError, /model-document-getRoot-root-not-exist/ );
 		} );
 	} );
 
-	describe( 'hasRoot()', () => {
-		it( 'should return true when Document has RootElement with given name', () => {
-			doc.createRoot();
-
-			expect( doc.hasRoot( 'main' ) ).to.be.true;
-		} );
-
-		it( 'should return false when Document does not have RootElement with given name', () => {
-			expect( doc.hasRoot( 'noroot' ) ).to.be.false;
-		} );
-	} );
-
-	describe( 'applyOperation()', () => {
+	describe( 'model#applyOperation listener', () => {
 		it( 'should increase document version, execute operation and fire event with proper data ' +
 			'when operation is a document operation', () => {
 			const changeCallback = sinon.spy();
@@ -130,7 +62,7 @@ describe( 'Document', () => {
 			batch.addDelta( delta );
 
 			doc.on( 'change', changeCallback );
-			doc.applyOperation( operation );
+			model.applyOperation( operation );
 
 			expect( doc.version ).to.equal( 1 );
 			expect( doc.history._deltas.length ).to.equal( 1 );
@@ -163,7 +95,7 @@ describe( 'Document', () => {
 			batch.addDelta( delta );
 
 			doc.on( 'change', changeCallback );
-			doc.applyOperation( operation );
+			model.applyOperation( operation );
 
 			expect( doc.version ).to.equal( 0 );
 			expect( doc.history._deltas.length ).to.equal( 0 );
@@ -174,86 +106,90 @@ describe( 'Document', () => {
 
 		it( 'should throw an error on the operation base version and the document version is different', () => {
 			const operation = {
-				baseVersion: 1
+				baseVersion: 1,
+				isDocumentOperation: true,
+				_execute: () => {}
 			};
 
 			expect(
 				() => {
-					doc.applyOperation( operation );
+					model.applyOperation( operation );
 				}
 			).to.throw( CKEditorError, /^model-document-applyOperation-wrong-version/ );
 		} );
 	} );
 
-	describe( 'batch()', () => {
-		it( 'should create a new batch with the document property', () => {
-			const batch = doc.batch();
-
-			expect( batch ).to.be.instanceof( Batch );
-			expect( batch ).to.have.property( 'document' ).that.equals( doc );
+	describe( 'getRootNames()', () => {
+		it( 'should return empty iterator if no roots exist', () => {
+			expect( count( doc.getRootNames() ) ).to.equal( 0 );
 		} );
 
-		it( 'should set given batch type', () => {
-			const batch = doc.batch( 'ignore' );
+		it( 'should return an iterator of all roots without the graveyard', () => {
+			doc.createRoot( '$root', 'a' );
+			doc.createRoot( '$root', 'b' );
 
-			expect( batch ).to.have.property( 'type' ).that.equals( 'ignore' );
+			expect( Array.from( doc.getRootNames() ) ).to.deep.equal( [ 'a', 'b' ] );
 		} );
 	} );
 
-	describe( 'enqueue()', () => {
-		it( 'should be executed immediately and fire changesDone event', () => {
-			const order = [];
-
-			doc.on( 'changesDone', () => order.push( 'done' ) );
-
-			doc.enqueueChanges( () => order.push( 'enqueue1' ) );
+	describe( 'createRoot()', () => {
+		it( 'should create a new RootElement with default element and root names, add it to roots map and return it', () => {
+			const root = doc.createRoot();
 
-			expect( order ).to.have.length( 2 );
-			expect( order[ 0 ] ).to.equal( 'enqueue1' );
-			expect( order[ 1 ] ).to.equal( 'done' );
+			expect( doc.roots.size ).to.equal( 2 );
+			expect( root ).to.be.instanceof( RootElement );
+			expect( root.maxOffset ).to.equal( 0 );
+			expect( root ).to.have.property( 'name', '$root' );
+			expect( root ).to.have.property( 'rootName', 'main' );
 		} );
 
-		it( 'should fire done every time queue is empty', () => {
-			const order = [];
+		it( 'should create a new RootElement with custom element and root names, add it to roots map and return it', () => {
+			const root = doc.createRoot( 'customElementName', 'customRootName' );
 
-			doc.on( 'changesDone', () => order.push( 'done' ) );
+			expect( doc.roots.size ).to.equal( 2 );
+			expect( root ).to.be.instanceof( RootElement );
+			expect( root.maxOffset ).to.equal( 0 );
+			expect( root ).to.have.property( 'name', 'customElementName' );
+			expect( root ).to.have.property( 'rootName', 'customRootName' );
+		} );
 
-			doc.enqueueChanges( () => order.push( 'enqueue1' ) );
-			doc.enqueueChanges( () => order.push( 'enqueue2' ) );
+		it( 'should throw an error when trying to create a second root with the same name', () => {
+			doc.createRoot( '$root', 'rootName' );
 
-			expect( order ).to.have.length( 4 );
-			expect( order[ 0 ] ).to.equal( 'enqueue1' );
-			expect( order[ 1 ] ).to.equal( 'done' );
-			expect( order[ 2 ] ).to.equal( 'enqueue2' );
-			expect( order[ 3 ] ).to.equal( 'done' );
+			expect(
+				() => {
+					doc.createRoot( '$root', 'rootName' );
+				}
+			).to.throw( CKEditorError, /model-document-createRoot-name-exists/ );
 		} );
+	} );
 
-		it( 'should put callbacks in the proper order', () => {
-			const order = [];
+	describe( 'getRoot()', () => {
+		it( 'should return a RootElement previously created with given name', () => {
+			const newRoot = doc.createRoot();
+			const getRoot = doc.getRoot();
 
-			doc.on( 'changesDone', () => order.push( 'done' ) );
+			expect( getRoot ).to.equal( newRoot );
+		} );
 
-			doc.enqueueChanges( () => {
-				order.push( 'enqueue1 start' );
-				doc.enqueueChanges( () => {
-					order.push( 'enqueue2 start' );
-					doc.enqueueChanges( () => order.push( 'enqueue4' ) );
-					order.push( 'enqueue2 end' );
-				} );
+		it( 'should throw an error when trying to get non-existent root', () => {
+			expect(
+				() => {
+					doc.getRoot( 'root' );
+				}
+			).to.throw( CKEditorError, /model-document-getRoot-root-not-exist/ );
+		} );
+	} );
 
-				doc.enqueueChanges( () => order.push( 'enqueue3' ) );
+	describe( 'hasRoot()', () => {
+		it( 'should return true when Document has RootElement with given name', () => {
+			doc.createRoot();
 
-				order.push( 'enqueue1 end' );
-			} );
+			expect( doc.hasRoot( 'main' ) ).to.be.true;
+		} );
 
-			expect( order ).to.have.length( 7 );
-			expect( order[ 0 ] ).to.equal( 'enqueue1 start' );
-			expect( order[ 1 ] ).to.equal( 'enqueue1 end' );
-			expect( order[ 2 ] ).to.equal( 'enqueue2 start' );
-			expect( order[ 3 ] ).to.equal( 'enqueue2 end' );
-			expect( order[ 4 ] ).to.equal( 'enqueue3' );
-			expect( order[ 5 ] ).to.equal( 'enqueue4' );
-			expect( order[ 6 ] ).to.equal( 'done' );
+		it( 'should return false when Document does not have RootElement with given name', () => {
+			expect( doc.hasRoot( 'noroot' ) ).to.be.false;
 		} );
 	} );
 
@@ -313,18 +249,18 @@ describe( 'Document', () => {
 		let selection;
 
 		beforeEach( () => {
-			doc.schema.registerItem( 'paragraph', '$block' );
+			model.schema.registerItem( 'paragraph', '$block' );
 
-			doc.schema.registerItem( 'emptyBlock' );
-			doc.schema.allow( { name: 'emptyBlock', inside: '$root' } );
+			model.schema.registerItem( 'emptyBlock' );
+			model.schema.allow( { name: 'emptyBlock', inside: '$root' } );
 
-			doc.schema.registerItem( 'widget' );
-			doc.schema.allow( { name: 'widget', inside: '$root' } );
-			doc.schema.objects.add( 'widget' );
+			model.schema.registerItem( 'widget' );
+			model.schema.allow( { name: 'widget', inside: '$root' } );
+			model.schema.objects.add( 'widget' );
 
-			doc.schema.registerItem( 'blockWidget', '$block' );
-			doc.schema.allow( { name: 'blockWidget', inside: '$root' } );
-			doc.schema.objects.add( 'blockWidget' );
+			model.schema.registerItem( 'blockWidget', '$block' );
+			model.schema.allow( { name: 'blockWidget', inside: '$root' } );
+			model.schema.objects.add( 'blockWidget' );
 
 			doc.createRoot();
 			selection = doc.selection;
@@ -524,14 +460,14 @@ describe( 'Document', () => {
 
 		function test( testName, data, direction, expected ) {
 			it( testName, () => {
-				setData( doc, data );
+				setData( model, data );
 				const range = doc.getNearestSelectionRange( selection.anchor, direction );
 
 				if ( expected === null ) {
 					expect( range ).to.be.null;
 				} else {
 					selection.setRanges( [ range ] );
-					expect( getData( doc ) ).to.equal( expected );
+					expect( getData( model ) ).to.equal( expected );
 				}
 			} );
 		}
@@ -590,6 +526,9 @@ describe( 'Document', () => {
 	} );
 
 	it( 'should be correctly converted to json', () => {
-		expect( jsonParseStringify( doc ).selection ).to.equal( '[engine.model.DocumentSelection]' );
+		const serialized = jsonParseStringify( doc );
+
+		expect( serialized.selection ).to.equal( '[engine.model.DocumentSelection]' );
+		expect( serialized.model ).to.equal( '[engine.model.Model]' );
 	} );
 } );

+ 90 - 71
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -3,7 +3,8 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../src/model/document';
+import Model from '../../src/model/model';
+import Batch from '../../src/model/batch';
 import Element from '../../src/model/element';
 import Text from '../../src/model/text';
 import Range from '../../src/model/range';
@@ -26,13 +27,14 @@ import log from '@ckeditor/ckeditor5-utils/src/log';
 testUtils.createSinonSandbox();
 
 describe( 'DocumentSelection', () => {
-	let doc, root, selection, liveRange, range;
+	let model, doc, root, selection, liveRange, range;
 
 	const fooStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'foo' );
 	const abcStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'abc' );
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 		root = doc.createRoot();
 		root.appendChildren( [
 			new Element( 'p' ),
@@ -44,17 +46,17 @@ describe( 'DocumentSelection', () => {
 			new Element( 'p', [], new Text( 'foobar' ) )
 		] );
 		selection = doc.selection;
-		doc.schema.registerItem( 'p', '$block' );
+		model.schema.registerItem( 'p', '$block' );
 
-		doc.schema.registerItem( 'widget' );
-		doc.schema.objects.add( 'widget' );
+		model.schema.registerItem( 'widget' );
+		model.schema.objects.add( 'widget' );
 
 		liveRange = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
 		range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 2, 2 ] ) );
 	} );
 
 	afterEach( () => {
-		doc.destroy();
+		model.destroy();
 		liveRange.detach();
 	} );
 
@@ -69,7 +71,8 @@ describe( 'DocumentSelection', () => {
 		} );
 
 		it( 'should be set to the beginning of the doc if there is no editable element', () => {
-			doc = new Document();
+			model = new Model();
+			doc = model.document;
 			root = doc.createRoot();
 			root.insertChildren( 0, new Text( 'foobar' ) );
 			selection = doc.selection;
@@ -84,14 +87,15 @@ describe( 'DocumentSelection', () => {
 		} );
 
 		it( 'should skip element when you can not put selection', () => {
-			doc = new Document();
+			model = new Model();
+			doc = model.document;
 			root = doc.createRoot();
 			root.insertChildren( 0, [
 				new Element( 'img' ),
 				new Element( 'p', [], new Text( 'foobar' ) )
 			] );
-			doc.schema.registerItem( 'img' );
-			doc.schema.registerItem( 'p', '$block' );
+			model.schema.registerItem( 'img' );
+			model.schema.registerItem( 'p', '$block' );
 			selection = doc.selection;
 
 			const ranges = Array.from( selection.getRanges() );
@@ -356,16 +360,16 @@ describe( 'DocumentSelection', () => {
 
 		// See #630.
 		it( 'should refresh attributes – integration test for #630', () => {
-			doc.schema.allow( { name: '$text', inside: '$root' } );
+			model.schema.allow( { name: '$text', inside: '$root' } );
 
-			setData( doc, 'f<$text italic="true">[o</$text><$text bold="true">ob]a</$text>r' );
+			setData( model, 'f<$text italic="true">[o</$text><$text bold="true">ob]a</$text>r' );
 
 			selection.setRanges( [ Range.createFromPositionAndShift( selection.getLastRange().end, 0 ) ] );
 
 			expect( selection.getAttribute( 'bold' ) ).to.equal( true );
 			expect( selection.hasAttribute( 'italic' ) ).to.equal( false );
 
-			expect( getData( doc ) )
+			expect( getData( model ) )
 				.to.equal( 'f<$text italic="true">o</$text><$text bold="true">ob[]a</$text>r' );
 		} );
 	} );
@@ -433,7 +437,7 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new InsertOperation(
 						new Position( root, [ 0, 1 ] ),
 						'xyz',
@@ -453,7 +457,7 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new InsertOperation(
 						new Position( root, [ 1, 0 ] ),
 						'xyz',
@@ -475,7 +479,7 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new MoveOperation(
 						new Position( root, [ 0, 0 ] ),
 						2,
@@ -496,7 +500,7 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new MoveOperation(
 						new Position( root, [ 2 ] ),
 						2,
@@ -517,7 +521,7 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new MoveOperation(
 						new Position( root, [ 1, 0 ] ),
 						2,
@@ -538,7 +542,7 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new MoveOperation(
 						new Position( root, [ 1, 3 ] ),
 						2,
@@ -559,7 +563,7 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				const batch = doc.batch();
+				const batch = new Batch();
 				const splitDelta = new SplitDelta();
 
 				const insertOperation = new InsertOperation(
@@ -580,8 +584,8 @@ describe( 'DocumentSelection', () => {
 				splitDelta.addOperation( insertOperation );
 				splitDelta.addOperation( moveOperation );
 
-				doc.applyOperation( insertOperation );
-				doc.applyOperation( moveOperation );
+				model.applyOperation( insertOperation );
+				model.applyOperation( moveOperation );
 
 				const range = selection.getFirstRange();
 
@@ -601,7 +605,7 @@ describe( 'DocumentSelection', () => {
 					expect( data.attributeKeys ).to.deep.equal( [ 'foo' ] );
 				} );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new AttributeOperation(
 						new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 5 ] ) ),
 						'foo',
@@ -621,7 +625,7 @@ describe( 'DocumentSelection', () => {
 				const spyAttribute = sinon.spy();
 				selection.on( 'change:attribute', spyAttribute );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new AttributeOperation(
 						new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 5 ] ) ),
 						'foo',
@@ -642,7 +646,7 @@ describe( 'DocumentSelection', () => {
 				const spyAttribute = sinon.spy();
 				selection.on( 'change:attribute', spyAttribute );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new AttributeOperation(
 						new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 5 ] ) ),
 						'foo',
@@ -661,7 +665,7 @@ describe( 'DocumentSelection', () => {
 			it( 'fix selection range if it ends up in graveyard #1', () => {
 				selection.setCollapsedAt( new Position( root, [ 1, 3 ] ) );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new RemoveOperation(
 						new Position( root, [ 1, 2 ] ),
 						2,
@@ -676,7 +680,7 @@ describe( 'DocumentSelection', () => {
 			it( 'fix selection range if it ends up in graveyard #2', () => {
 				selection.setRanges( [ new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 1, 4 ] ) ) ] );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new RemoveOperation(
 						new Position( root, [ 1, 2 ] ),
 						2,
@@ -691,7 +695,7 @@ describe( 'DocumentSelection', () => {
 			it( 'fix selection range if it ends up in graveyard #3', () => {
 				selection.setRanges( [ new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ) ] );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new RemoveOperation(
 						new Position( root, [ 1 ] ),
 						2,
@@ -704,7 +708,7 @@ describe( 'DocumentSelection', () => {
 			} );
 
 			it( 'fix selection range if it ends up in graveyard #4 - whole content removed', () => {
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new RemoveOperation(
 						new Position( root, [ 0 ] ),
 						3,
@@ -715,7 +719,7 @@ describe( 'DocumentSelection', () => {
 
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 0 ] );
 
-				doc.applyOperation( wrapInDelta(
+				model.applyOperation( wrapInDelta(
 					new InsertOperation(
 						new Position( root, [ 0 ] ),
 						new Element( 'p' ),
@@ -937,50 +941,50 @@ describe( 'DocumentSelection', () => {
 		// #986
 		describe( 'are not inherited from the inside of object elements', () => {
 			beforeEach( () => {
-				doc.schema.registerItem( 'image' );
-				doc.schema.allow( { name: 'image', inside: '$root' } );
-				doc.schema.allow( { name: 'image', inside: '$block' } );
-				doc.schema.allow( { name: '$inline', inside: 'image' } );
-				doc.schema.objects.add( 'image' );
-
-				doc.schema.registerItem( 'caption' );
-				doc.schema.allow( { name: '$inline', inside: 'caption' } );
-				doc.schema.allow( { name: 'caption', inside: 'image' } );
-				doc.schema.allow( { name: '$text', attributes: 'bold', inside: 'caption' } );
+				model.schema.registerItem( 'image' );
+				model.schema.allow( { name: 'image', inside: '$root' } );
+				model.schema.allow( { name: 'image', inside: '$block' } );
+				model.schema.allow( { name: '$inline', inside: 'image' } );
+				model.schema.objects.add( 'image' );
+
+				model.schema.registerItem( 'caption' );
+				model.schema.allow( { name: '$inline', inside: 'caption' } );
+				model.schema.allow( { name: 'caption', inside: 'image' } );
+				model.schema.allow( { name: '$text', attributes: 'bold', inside: 'caption' } );
 			} );
 
 			it( 'ignores attributes inside an object if selection contains that object', () => {
-				setData( doc, '<p>[<image><$text bold="true">Caption for the image.</$text></image>]</p>' );
+				setData( model, '<p>[<image><$text bold="true">Caption for the image.</$text></image>]</p>' );
 
 				expect( selection.hasAttribute( 'bold' ) ).to.equal( false );
 			} );
 
 			it( 'ignores attributes inside an object if selection contains that object (deeper structure)', () => {
-				setData( doc, '<p>[<image><caption><$text bold="true">Caption for the image.</$text></caption></image>]</p>' );
+				setData( model, '<p>[<image><caption><$text bold="true">Caption for the image.</$text></caption></image>]</p>' );
 
 				expect( selection.hasAttribute( 'bold' ) ).to.equal( false );
 			} );
 
 			it( 'ignores attributes inside an object if selection contains that object (block level)', () => {
-				setData( doc, '<p>foo</p>[<image><$text bold="true">Caption for the image.</$text></image>]<p>foo</p>' );
+				setData( model, '<p>foo</p>[<image><$text bold="true">Caption for the image.</$text></image>]<p>foo</p>' );
 
 				expect( selection.hasAttribute( 'bold' ) ).to.equal( false );
 			} );
 
 			it( 'reads attributes from text even if the selection contains an object', () => {
-				setData( doc, '<p>x<$text bold="true">[bar</$text><image></image>foo]</p>' );
+				setData( model, '<p>x<$text bold="true">[bar</$text><image></image>foo]</p>' );
 
 				expect( selection.getAttribute( 'bold' ) ).to.equal( true );
 			} );
 
 			it( 'reads attributes when the entire selection inside an object', () => {
-				setData( doc, '<p><image><caption><$text bold="true">[bar]</$text></caption></image></p>' );
+				setData( model, '<p><image><caption><$text bold="true">[bar]</$text></caption></image></p>' );
 
 				expect( selection.getAttribute( 'bold' ) ).to.equal( true );
 			} );
 
 			it( 'stops reading attributes if selection starts with an object', () => {
-				setData( doc, '<p>[<image></image><$text bold="true">bar]</$text></p>' );
+				setData( model, '<p>[<image></image><$text bold="true">bar]</$text></p>' );
 
 				expect( selection.hasAttribute( 'bold' ) ).to.equal( false );
 			} );
@@ -1012,7 +1016,9 @@ describe( 'DocumentSelection', () => {
 					batchTypes.set( batch, batch.type );
 				} );
 
-				doc.batch().insertText( 'x', rangeInEmptyP.start );
+				model.change( writer => {
+					writer.insertText( 'x', rangeInEmptyP.start );
+				} );
 
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
 				expect( emptyP.hasAttribute( abcStoreAttrKey ) ).to.be.false;
@@ -1024,7 +1030,9 @@ describe( 'DocumentSelection', () => {
 				selection.setRanges( [ rangeInEmptyP ] );
 				selection.setAttribute( 'foo', 'bar' );
 
-				doc.batch().move( Range.createOn( fullP.getChild( 0 ) ), rangeInEmptyP.start );
+				model.change( writer => {
+					writer.move( Range.createOn( fullP.getChild( 0 ) ), rangeInEmptyP.start );
+				} );
 
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
 			} );
@@ -1036,8 +1044,10 @@ describe( 'DocumentSelection', () => {
 				emptyP.setAttribute( fooStoreAttrKey, 'bar' );
 				emptyP2.setAttribute( fooStoreAttrKey, 'bar' );
 
-				// <emptyP>{}<emptyP2>
-				doc.batch().merge( Position.createAfter( emptyP ) );
+				model.change( writer => {
+					// <emptyP>{}<emptyP2>
+					writer.merge( Position.createAfter( emptyP ) );
+				} );
 
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
 				expect( emptyP.parent ).to.equal( root ); // Just to be sure we're checking the right element.
@@ -1048,35 +1058,38 @@ describe( 'DocumentSelection', () => {
 
 				selection.setRanges( [ rangeInFullP ] );
 
-				doc.batch().insertText( 'x', rangeInEmptyP.start );
+				model.change( writer => {
+					writer.insertText( 'x', rangeInEmptyP.start );
+				} );
 
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
 			} );
 
 			it( 'are removed only once in case of multi-op deltas', () => {
+				let batch;
 				const emptyP2 = new Element( 'p', null, 'x' );
 				root.appendChildren( emptyP2 );
 
 				emptyP.setAttribute( fooStoreAttrKey, 'bar' );
 				emptyP2.setAttribute( fooStoreAttrKey, 'bar' );
 
-				const batch = doc.batch();
-				const spy = sinon.spy( batch, 'removeAttribute' );
-
-				// <emptyP>{}<emptyP2>
-				batch.merge( Position.createAfter( emptyP ) );
+				model.change( writer => {
+					batch = writer.batch;
+					// <emptyP>{}<emptyP2>
+					writer.merge( Position.createAfter( emptyP ) );
+				} );
 
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
-
-				expect( spy.calledOnce ).to.be.true;
+				// Attribute delta is only one.
+				expect( Array.from( batch.deltas, delta => delta.type ) ).to.deep.equal( [ 'merge', 'attribute' ] );
 			} );
 
-			it( 'uses document enqueue changes to clear attributes', () => {
+			it( 'uses model change to clear attributes', () => {
 				selection.setRanges( [ rangeInEmptyP ] );
 				selection.setAttribute( 'foo', 'bar' );
 
-				doc.enqueueChanges( () => {
-					doc.batch().insertText( 'x', rangeInEmptyP.start );
+				model.change( writer => {
+					writer.insertText( 'x', rangeInEmptyP.start );
 
 					// `emptyP` still has the attribute, because attribute clearing is in enqueued block.
 					expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.true;
@@ -1096,8 +1109,10 @@ describe( 'DocumentSelection', () => {
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.true;
 				expect( emptyP.hasAttribute( abcStoreAttrKey ) ).to.be.false;
 
-				// <emptyP>{}<emptyP2>
-				doc.batch().merge( Position.createAfter( emptyP ) );
+				model.change( writer => {
+					// <emptyP>{}<emptyP2>
+					writer.merge( Position.createAfter( emptyP ) );
+				} );
 
 				expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
 				expect( emptyP.parent ).to.equal( root ); // Just to be sure we're checking the right element.
@@ -1107,12 +1122,14 @@ describe( 'DocumentSelection', () => {
 				selection.setRanges( [ rangeInEmptyP ] );
 				selection.setAttribute( 'foo', 'bar' );
 
-				sinon.spy( doc, 'enqueueChanges' );
+				model.enqueueChange( 'transparent', writer => {
+					sinon.spy( model, 'enqueueChange' );
 
-				doc.batch( 'transparent' ).insertText( 'x', rangeInEmptyP.start );
+					writer.insertText( 'x', rangeInEmptyP.start );
 
-				expect( doc.enqueueChanges.called ).to.be.false;
-				expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
+					expect( model.enqueueChange.called ).to.be.false;
+					expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
+				} );
 			} );
 
 			// Rename and some other deltas don't specify range in doc#change event.
@@ -1121,11 +1138,13 @@ describe( 'DocumentSelection', () => {
 				selection.setRanges( [ rangeInEmptyP ] );
 				selection.setAttribute( 'foo', 'bar' );
 
-				sinon.spy( doc, 'enqueueChanges' );
+				sinon.spy( model, 'enqueueChange' );
 
-				doc.batch().rename( emptyP, 'pnew' );
+				model.change( writer => {
+					writer.rename( emptyP, 'pnew' );
+				} );
 
-				expect( doc.enqueueChanges.called ).to.be.false;
+				expect( model.enqueueChange.called ).to.be.false;
 				expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
 			} );
 		} );

+ 4 - 2
packages/ckeditor5-engine/tests/model/liveposition.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../src/model/document';
+import Model from '../../src/model/model';
 import DocumentFragment from '../../src/model/documentfragment';
 import Element from '../../src/model/element';
 import Text from '../../src/model/text';
@@ -16,7 +16,9 @@ describe( 'LivePosition', () => {
 	let doc, root, ul, p, li1, li2;
 
 	before( () => {
-		doc = new Document();
+		const model = new Model();
+
+		doc = model.document;
 		root = doc.createRoot();
 
 		li1 = new Element( 'li', [], new Text( 'abcdef' ) );

+ 66 - 41
packages/ckeditor5-engine/tests/model/liverange.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../src/model/document';
+import Model from '../../src/model/model';
 import Element from '../../src/model/element';
 import Position from '../../src/model/position';
 import LiveRange from '../../src/model/liverange';
@@ -12,10 +12,11 @@ import Text from '../../src/model/text';
 import { stringify, setData } from '../../src/dev-utils/model';
 
 describe( 'LiveRange', () => {
-	let doc, root, ul, p;
+	let model, doc, root, ul, p;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 		root = doc.createRoot();
 
 		const lis = [
@@ -429,11 +430,11 @@ describe( 'LiveRange', () => {
 			let live;
 
 			beforeEach( () => {
-				doc.schema.registerItem( 'p', '$block' );
-				doc.schema.registerItem( 'w' );
+				model.schema.registerItem( 'p', '$block' );
+				model.schema.registerItem( 'w' );
 
-				doc.schema.allow( { name: 'p', inside: 'w' } );
-				doc.schema.allow( { name: 'w', inside: '$root' } );
+				model.schema.allow( { name: 'p', inside: 'w' } );
+				model.schema.allow( { name: 'w', inside: '$root' } );
 			} );
 
 			afterEach( () => {
@@ -441,67 +442,79 @@ describe( 'LiveRange', () => {
 			} );
 
 			it( 'is inside the wrapped range', () => {
-				setData( doc, '<p>x</p><p>[a]</p><p>x</p>' );
+				setData( model, '<p>x</p><p>[a]</p><p>x</p>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				// [<p>a</p>]
-				doc.batch().wrap( new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) ), 'w' );
+				model.change( writer => {
+					// [<p>a</p>]
+					writer.wrap( new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) ), 'w' );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<p>x</p><w><p>[a]</p></w><p>x</p>' );
 			} );
 
 			it( 'its start is intersecting with the wrapped range', () => {
-				setData( doc, '<p>a[b</p><p>x</p><p>c]d</p>' );
+				setData( model, '<p>a[b</p><p>x</p><p>c]d</p>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				// [<p>ab</p>]
-				doc.batch().wrap( new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ), 'w' );
+				model.change( writer => {
+					// [<p>ab</p>]
+					writer.wrap( new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ), 'w' );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<w><p>a[b</p></w><p>x</p><p>c]d</p>' );
 			} );
 
 			it( 'its end is intersecting with the wrapped range', () => {
-				setData( doc, '<p>a[b</p><p>x</p><p>c]d</p>' );
+				setData( model, '<p>a[b</p><p>x</p><p>c]d</p>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				// [<p>cd</p>]
-				doc.batch().wrap( new Range( new Position( root, [ 2 ] ), new Position( root, [ 3 ] ) ), 'w' );
+				model.change( writer => {
+					// [<p>cd</p>]
+					writer.wrap( new Range( new Position( root, [ 2 ] ), new Position( root, [ 3 ] ) ), 'w' );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<p>a[b</p><p>x</p><w><p>c]d</p></w>' );
 			} );
 
 			it( 'its start is intersecting with the wrapped range (multilpe elements)', () => {
-				setData( doc, '<p>a[b</p><p>x</p><p>c]d</p>' );
+				setData( model, '<p>a[b</p><p>x</p><p>c]d</p>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				// [<p>ab</p><p>x</p>]
-				doc.batch().wrap( new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ), 'w' );
+				model.change( writer => {
+					// [<p>ab</p><p>x</p>]
+					writer.wrap( new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ), 'w' );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<w><p>a[b</p><p>x</p></w><p>c]d</p>' );
 			} );
 
 			it( 'its end is intersecting with the wrapped range (multiple elements)', () => {
-				setData( doc, '<p>a[b</p><p>x</p><p>c]d</p>' );
+				setData( model, '<p>a[b</p><p>x</p><p>c]d</p>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				// [<p>x</p><p>cd</p>]
-				doc.batch().wrap( new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) ), 'w' );
+				model.change( writer => {
+					// [<p>x</p><p>cd</p>]
+					writer.wrap( new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) ), 'w' );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<p>a[b</p><w><p>x</p><p>c]d</p></w>' );
 			} );
 
 			it( 'contains element to wrap', () => {
-				setData( doc, '<p>a[b</p><p>x</p><p>c]d</p>' );
+				setData( model, '<p>a[b</p><p>x</p><p>c]d</p>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				// [<p>x</p>]
-				doc.batch().wrap( new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) ), 'w' );
+				model.change( writer => {
+					// [<p>x</p>]
+					writer.wrap( new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) ), 'w' );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<p>a[b</p><w><p>x</p></w><p>c]d</p>' );
 			} );
@@ -513,11 +526,11 @@ describe( 'LiveRange', () => {
 			let live;
 
 			beforeEach( () => {
-				doc.schema.registerItem( 'p', '$block' );
-				doc.schema.registerItem( 'w' );
+				model.schema.registerItem( 'p', '$block' );
+				model.schema.registerItem( 'w' );
 
-				doc.schema.allow( { name: 'p', inside: 'w' } );
-				doc.schema.allow( { name: 'w', inside: '$root' } );
+				model.schema.allow( { name: 'p', inside: 'w' } );
+				model.schema.allow( { name: 'w', inside: '$root' } );
 			} );
 
 			afterEach( () => {
@@ -525,61 +538,73 @@ describe( 'LiveRange', () => {
 			} );
 
 			it( 'is inside the wrapper to remove', () => {
-				setData( doc, '<p>x</p><w><p>[a]</p></w><p>x</p>' );
+				setData( model, '<p>x</p><w><p>[a]</p></w><p>x</p>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				doc.batch().unwrap( root.getChild( 1 ) );
+				model.change( writer => {
+					writer.unwrap( root.getChild( 1 ) );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<p>x</p><p>[a]</p><p>x</p>' );
 			} );
 
 			it( 'its start is intersecting with the wrapper to remove', () => {
-				setData( doc, '<w><p>a[b</p></w><p>c]d</p>' );
+				setData( model, '<w><p>a[b</p></w><p>c]d</p>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				doc.batch().unwrap( root.getChild( 0 ) );
+				model.change( writer => {
+					writer.unwrap( root.getChild( 0 ) );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<p>a[b</p><p>c]d</p>' );
 			} );
 
 			it( 'its end is intersecting with the wrapper to remove', () => {
-				setData( doc, '<p>a[b</p><w><p>c]d</p></w>' );
+				setData( model, '<p>a[b</p><w><p>c]d</p></w>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				doc.batch().unwrap( root.getChild( 1 ) );
+				model.change( writer => {
+					writer.unwrap( root.getChild( 1 ) );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<p>a[b</p><p>c]d</p>' );
 			} );
 
 			it( 'its start is intersecting with the wrapper to remove (multiple elements)', () => {
-				setData( doc, '<w><p>a[b</p><p>x</p></w><p>c]d</p>' );
+				setData( model, '<w><p>a[b</p><p>x</p></w><p>c]d</p>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				doc.batch().unwrap( root.getChild( 0 ) );
+				model.change( writer => {
+					writer.unwrap( root.getChild( 0 ) );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<p>a[b</p><p>x</p><p>c]d</p>' );
 			} );
 
 			it( 'its end is intersecting with the wrapper to remove (multiple elements)', () => {
-				setData( doc, '<p>a[b</p><w><p>x</p><p>c]d</p></w>' );
+				setData( model, '<p>a[b</p><w><p>x</p><p>c]d</p></w>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				doc.batch().unwrap( root.getChild( 1 ) );
+				model.change( writer => {
+					writer.unwrap( root.getChild( 1 ) );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<p>a[b</p><p>x</p><p>c]d</p>' );
 			} );
 
 			it( 'contains wrapped element', () => {
-				setData( doc, '<p>a[b</p><w><p>x</p></w><p>c]d</p>' );
+				setData( model, '<p>a[b</p><w><p>x</p></w><p>c]d</p>' );
 
 				live = new LiveRange( doc.selection.getFirstPosition(), doc.selection.getLastPosition() );
 
-				doc.batch().unwrap( root.getChild( 1 ) );
+				model.change( writer => {
+					writer.unwrap( root.getChild( 1 ) );
+				} );
 
 				expect( stringify( root, live ) ).to.equal( '<p>a[b</p><p>x</p><p>c]d</p>' );
 			} );

+ 13 - 10
packages/ckeditor5-engine/tests/model/markercollection.js

@@ -7,14 +7,16 @@ import MarkerCollection from '../../src/model/markercollection';
 import Position from '../../src/model/position';
 import Range from '../../src/model/range';
 import Text from '../../src/model/text';
-import Document from '../../src/model/document';
+import Model from '../../src/model/model';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 describe( 'MarkerCollection', () => {
 	let markers, range, range2, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		const model = new Model();
+
+		doc = model.document;
 		markers = new MarkerCollection();
 
 		root = doc.createRoot();
@@ -207,10 +209,11 @@ describe( 'MarkerCollection', () => {
 } );
 
 describe( 'Marker', () => {
-	let doc, root;
+	let model, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 		root = doc.createRoot();
 	} );
 
@@ -218,14 +221,14 @@ describe( 'Marker', () => {
 		root.appendChildren( new Text( 'foo' ) );
 
 		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
-		const marker = doc.markers.set( 'name', range );
+		const marker = model.markers.set( 'name', range );
 
 		expect( marker.getRange().isEqual( range ) ).to.be.true;
 		expect( marker.getStart().isEqual( range.start ) ).to.be.true;
 		expect( marker.getEnd().isEqual( range.end ) ).to.be.true;
 
-		doc.enqueueChanges( () => {
-			doc.batch().insertText( 'abc', root );
+		model.change( writer => {
+			writer.insertText( 'abc', root );
 		} );
 
 		const updatedRange = Range.createFromParentsAndOffsets( root, 4, root, 5 );
@@ -237,9 +240,9 @@ describe( 'Marker', () => {
 
 	it( 'should throw when using the API if marker was removed from markers collection', () => {
 		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
-		const marker = doc.markers.set( 'name', range );
+		const marker = model.markers.set( 'name', range );
 
-		doc.markers.remove( 'name' );
+		model.markers.remove( 'name' );
 
 		expect( () => {
 			marker.getRange();
@@ -256,7 +259,7 @@ describe( 'Marker', () => {
 
 	it( 'should delegate events from live range', () => {
 		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
-		const marker = doc.markers.set( 'name', range );
+		const marker = model.markers.set( 'name', range );
 
 		const eventRange = sinon.spy();
 		const eventContent = sinon.spy();

+ 346 - 0
packages/ckeditor5-engine/tests/model/model.js

@@ -0,0 +1,346 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Model from '../../src/model/model';
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+
+describe( 'Model', () => {
+	let model;
+	let changes = '';
+
+	beforeEach( () => {
+		model = new Model();
+		changes = '';
+	} );
+
+	describe( 'change & enqueueChange', () => {
+		it( 'should execute changes immediately', () => {
+			model.change( () => {
+				changes += 'A';
+			} );
+
+			expect( changes ).to.equal( 'A' );
+		} );
+
+		it( 'should pass returned value', () => {
+			const ret = model.change( () => {
+				changes += 'A';
+
+				return 'B';
+			} );
+
+			changes += ret;
+
+			expect( changes ).to.equal( 'AB' );
+		} );
+
+		it( 'should not mixed the order when nested change is called', () => {
+			const ret = model.change( () => {
+				changes += 'A';
+
+				nested();
+
+				return 'D';
+			} );
+
+			changes += ret;
+
+			expect( changes ).to.equal( 'ABCD' );
+
+			function nested() {
+				const ret = model.change( () => {
+					changes += 'B';
+
+					return 'C';
+				} );
+
+				changes += ret;
+			}
+		} );
+
+		it( 'should execute enqueueChange immediately if its the first block', () => {
+			model.enqueueChange( () => {
+				changes += 'A';
+
+				nested();
+			} );
+
+			expect( changes ).to.equal( 'ABC' );
+
+			function nested() {
+				const ret = model.change( () => {
+					changes += 'B';
+
+					return 'C';
+				} );
+
+				changes += ret;
+			}
+		} );
+
+		it( 'should be possible to enqueueChange immediately if its the first block', () => {
+			model.enqueueChange( () => {
+				changes += 'A';
+
+				nested();
+			} );
+
+			expect( changes ).to.equal( 'AB' );
+
+			function nested() {
+				model.change( () => {
+					changes += 'B';
+				} );
+			}
+		} );
+
+		it( 'should be possible to nest change in enqueueChange', () => {
+			model.enqueueChange( () => {
+				changes += 'A';
+
+				nested();
+
+				changes += 'D';
+			} );
+
+			expect( changes ).to.equal( 'ABCD' );
+
+			function nested() {
+				const ret = model.change( () => {
+					changes += 'B';
+
+					return 'C';
+				} );
+
+				changes += ret;
+			}
+		} );
+
+		it( 'should be possible to nest enqueueChange in enqueueChange', () => {
+			model.enqueueChange( () => {
+				changes += 'A';
+
+				nestedEnqueue();
+
+				changes += 'B';
+			} );
+
+			expect( changes ).to.equal( 'ABC' );
+
+			function nestedEnqueue() {
+				model.enqueueChange( () => {
+					changes += 'C';
+				} );
+			}
+		} );
+
+		it( 'should be possible to nest enqueueChange in changes', () => {
+			const ret = model.change( () => {
+				changes += 'A';
+
+				nestedEnqueue();
+
+				changes += 'B';
+
+				return 'D';
+			} );
+
+			changes += ret;
+
+			expect( changes ).to.equal( 'ABCD' );
+
+			function nestedEnqueue() {
+				model.enqueueChange( () => {
+					changes += 'C';
+				} );
+			}
+		} );
+
+		it( 'should be possible to nest enqueueChange in enqueueChange event', () => {
+			model.once( 'change', () => {
+				model.enqueueChange( () => {
+					changes += 'C';
+				} );
+
+				changes += 'B';
+			} );
+
+			model.on( 'changesDone', () => {
+				changes += 'D';
+			} );
+
+			model.enqueueChange( () => {
+				changes += 'A';
+			} );
+
+			expect( changes ).to.equal( 'ABCD' );
+		} );
+
+		it( 'should be possible to nest enqueueChange in changes event', () => {
+			model.once( 'change', () => {
+				model.enqueueChange( () => {
+					changes += 'C';
+				} );
+
+				changes += 'B';
+			} );
+
+			model.on( 'changesDone', () => {
+				changes += 'D';
+			} );
+
+			model.change( () => {
+				changes += 'A';
+			} );
+
+			expect( changes ).to.equal( 'ABCD' );
+		} );
+
+		it( 'should be possible to nest changes in enqueueChange event', () => {
+			model.once( 'change', () => {
+				model.change( () => {
+					changes += 'B';
+				} );
+
+				changes += 'C';
+			} );
+
+			model.on( 'changesDone', () => {
+				changes += 'D';
+			} );
+
+			model.enqueueChange( () => {
+				changes += 'A';
+			} );
+
+			expect( changes ).to.equal( 'ABCD' );
+		} );
+
+		it( 'should be possible to nest changes in changes event', () => {
+			model.once( 'change', () => {
+				model.change( () => {
+					changes += 'B';
+				} );
+
+				changes += 'C';
+			} );
+
+			model.on( 'changesDone', () => {
+				changes += 'D';
+			} );
+
+			model.change( () => {
+				changes += 'A';
+			} );
+
+			expect( changes ).to.equal( 'ABCD' );
+		} );
+
+		it( 'should let mix blocks', () => {
+			model.once( 'change', () => {
+				model.change( () => {
+					changes += 'B';
+
+					nestedEnqueue();
+				} );
+
+				model.change( () => {
+					changes += 'C';
+				} );
+
+				changes += 'D';
+			} );
+
+			model.on( 'changesDone', () => {
+				changes += 'F';
+			} );
+
+			model.change( () => {
+				changes += 'A';
+			} );
+
+			expect( changes ).to.equal( 'ABCDEF' );
+
+			function nestedEnqueue() {
+				model.enqueueChange( () => {
+					changes += 'E';
+				} );
+			}
+		} );
+
+		it( 'should use the same writer in all change blocks (change & change)', () => {
+			model.change( outerWriter => {
+				model.change( innerWriter => {
+					expect( innerWriter ).to.equal( outerWriter );
+				} );
+			} );
+		} );
+
+		it( 'should create new writer in enqueue block', () => {
+			model.change( outerWriter => {
+				model.enqueueChange( innerWriter => {
+					expect( innerWriter ).to.not.equal( outerWriter );
+					expect( innerWriter.batch ).to.not.equal( outerWriter.batch );
+				} );
+			} );
+		} );
+
+		it( 'should let you pass batch', () => {
+			let outerBatch;
+
+			model.change( outerWriter => {
+				outerBatch = outerWriter.batch;
+
+				model.enqueueChange( outerBatch, innerWriter => {
+					expect( innerWriter.batch ).to.equal( outerBatch );
+				} );
+			} );
+		} );
+
+		it( 'should let you create transparent batch', () => {
+			model.enqueueChange( 'transparent', writer => {
+				expect( writer.batch.type ).to.equal( 'transparent' );
+			} );
+		} );
+	} );
+
+	describe( 'applyOperation', () => {
+		it( 'should execute provided operation end return the result of operation', () => {
+			const returnValue = { foo: 'bar' };
+
+			const operation = {
+				_execute: sinon.stub().returns( returnValue )
+			};
+
+			model.applyOperation( operation );
+
+			sinon.assert.calledOnce( operation._execute );
+			expect( model.applyOperation( operation ) ).to.equal( returnValue );
+		} );
+	} );
+
+	describe( 'destroy()', () => {
+		it( 'should destroy document', () => {
+			sinon.spy( model.document, 'destroy' );
+
+			model.destroy();
+
+			sinon.assert.calledOnce( model.document.destroy );
+		} );
+
+		it( 'should stop listening', () => {
+			const emitter = Object.create( EmitterMixin );
+			const spy = sinon.spy();
+
+			model.listenTo( emitter, 'event', spy );
+
+			model.destroy();
+
+			emitter.fire( 'event' );
+
+			sinon.assert.notCalled( spy );
+		} );
+	} );
+} );

+ 4 - 2
packages/ckeditor5-engine/tests/model/node.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../src/model/document';
+import Model from '../../src/model/model';
 import DocumentFragment from '../../src/model/documentfragment';
 import Node from '../../src/model/node';
 import Element from '../../src/model/element';
@@ -17,6 +17,8 @@ describe( 'Node', () => {
 		textBA, textR, img;
 
 	beforeEach( () => {
+		const model = new Model();
+
 		node = new Node();
 
 		one = new Element( 'one' );
@@ -26,7 +28,7 @@ describe( 'Node', () => {
 		textR = two.getChild( 2 );
 		three = new Element( 'three' );
 
-		doc = new Document();
+		doc = model.document;
 		root = doc.createRoot();
 		root.appendChildren( [ one, two, three ] );
 	} );

+ 24 - 23
packages/ckeditor5-engine/tests/model/operation/attributeoperation.js

@@ -3,7 +3,8 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
+import DocumentFragment from '../../../src/model/documentfragment';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
 import AttributeOperation from '../../../src/model/operation/attributeoperation';
@@ -14,10 +15,11 @@ import count from '@ckeditor/ckeditor5-utils/src/count';
 import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
 
 describe( 'AttributeOperation', () => {
-	let doc, root;
+	let model, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 		root = doc.createRoot();
 	} );
 
@@ -73,8 +75,7 @@ describe( 'AttributeOperation', () => {
 		} );
 
 		it( 'should return false when attribute is applied on detached items', () => {
-			const docFrag = doc.batch().createDocumentFragment();
-			doc.batch().appendText( 'abc', null, docFrag );
+			const docFrag = new DocumentFragment( [ new Text( 'abc' ) ] );
 
 			const op = new AttributeOperation(
 				Range.createIn( docFrag ),
@@ -91,7 +92,7 @@ describe( 'AttributeOperation', () => {
 	it( 'should insert attribute to the set of nodes', () => {
 		root.insertChildren( 0, new Text( 'bar' ) );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ),
 				'isNew',
@@ -112,7 +113,7 @@ describe( 'AttributeOperation', () => {
 	it( 'should add attribute to the existing attributes', () => {
 		root.insertChildren( 0, new Text( 'x', { foo: true, bar: true } ) );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 				'isNew',
@@ -133,7 +134,7 @@ describe( 'AttributeOperation', () => {
 	it( 'should change attribute to the set of nodes', () => {
 		root.insertChildren( 0, new Text( 'bar', { isNew: false } ) );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ),
 				'isNew',
@@ -154,7 +155,7 @@ describe( 'AttributeOperation', () => {
 	it( 'should change attribute in the middle of existing attributes', () => {
 		root.insertChildren( 0, new Text( 'x', { foo: true, x: 1, bar: true } ) );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 				'x',
@@ -175,7 +176,7 @@ describe( 'AttributeOperation', () => {
 	it( 'should remove attribute', () => {
 		root.insertChildren( 0, new Text( 'x', { foo: true, x: true, bar: true } ) );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 				'x',
@@ -196,7 +197,7 @@ describe( 'AttributeOperation', () => {
 		root.insertChildren( 0, new Text( 'x', { foo: [ 'bar', 'xyz' ] } ) );
 
 		expect( () => {
-			doc.applyOperation( wrapInDelta(
+			model.applyOperation( wrapInDelta(
 				new AttributeOperation(
 					new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 					'foo',
@@ -234,8 +235,8 @@ describe( 'AttributeOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		doc.applyOperation( wrapInDelta( operation ) );
-		doc.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( wrapInDelta( reverse ) );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 3 );
@@ -248,7 +249,7 @@ describe( 'AttributeOperation', () => {
 
 		root.insertChildren( 0, [ eleA, eleB ] );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 2 ] ) ),
 				'foo',
@@ -269,7 +270,7 @@ describe( 'AttributeOperation', () => {
 
 		root.insertChildren( 0, [ eleA, eleB ] );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 1, 0 ] ) ),
 				'foo',
@@ -295,8 +296,8 @@ describe( 'AttributeOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		doc.applyOperation( wrapInDelta( operation ) );
-		doc.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( wrapInDelta( reverse ) );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 3 );
@@ -317,8 +318,8 @@ describe( 'AttributeOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		doc.applyOperation( wrapInDelta( operation ) );
-		doc.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( wrapInDelta( reverse ) );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 3 );
@@ -330,7 +331,7 @@ describe( 'AttributeOperation', () => {
 		root.insertChildren( 0, new Text( 'x' ) );
 
 		expect( () => {
-			doc.applyOperation( wrapInDelta(
+			model.applyOperation( wrapInDelta(
 				new AttributeOperation(
 					new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 					'foo',
@@ -346,7 +347,7 @@ describe( 'AttributeOperation', () => {
 		root.insertChildren( 0, new Text( 'x', { x: 1 } ) );
 
 		expect( () => {
-			doc.applyOperation( wrapInDelta(
+			model.applyOperation( wrapInDelta(
 				new AttributeOperation(
 					new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 					'x',
@@ -384,7 +385,7 @@ describe( 'AttributeOperation', () => {
 		root.insertChildren( 0, new Text( 'abc', attrA ) );
 		root.insertChildren( 1, new Text( 'xyz', attrB ) );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new AttributeOperation(
 				new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) ),
 				'foo',
@@ -402,7 +403,7 @@ describe( 'AttributeOperation', () => {
 		root.insertChildren( 0, new Text( 'x', { foo: true } ) );
 
 		expect( () => {
-			doc.applyOperation( wrapInDelta(
+			model.applyOperation( wrapInDelta(
 				new AttributeOperation(
 					new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 					'foo',

+ 12 - 11
packages/ckeditor5-engine/tests/model/operation/detachoperation.js

@@ -3,22 +3,22 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import DetachOperation from '../../../src/model/operation/detachoperation';
 import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Position from '../../../src/model/position';
+import DocumentFragment from '../../../src/model/documentfragment';
+import Element from '../../../src/model/element';
 
 describe( 'DetachOperation', () => {
-	let doc, batch, docFrag, element;
+	let model, doc, docFrag, element;
 
 	beforeEach( () => {
-		doc = new Document();
-		batch = doc.batch();
-
-		docFrag = batch.createDocumentFragment();
-		element = batch.createElement( 'element' );
-		batch.append( element, docFrag );
+		model = new Model();
+		doc = model.document;
+		element = new Element( 'element' );
+		docFrag = new DocumentFragment( [ element ] );
 	} );
 
 	it( 'should have type equal to detach', () => {
@@ -30,15 +30,16 @@ describe( 'DetachOperation', () => {
 	it( 'should remove given element from parent', () => {
 		const op = new DetachOperation( Position.createBefore( element ), 1, doc.version );
 
-		doc.applyOperation( wrapInDelta( op ) );
+		model.applyOperation( wrapInDelta( op ) );
 
 		expect( docFrag.childCount ).to.equal( 0 );
 	} );
 
 	it( 'should throw when is executed on element from document', () => {
 		const root = doc.createRoot();
-		const element = batch.createElement( 'element' );
-		batch.append( element, root );
+		const element = new Element( 'element' );
+
+		root.appendChildren( [ element ] );
 
 		const op = new DetachOperation( Position.createBefore( element ), 1, doc.version );
 

+ 17 - 15
packages/ckeditor5-engine/tests/model/operation/insertoperation.js

@@ -3,9 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import NodeList from '../../../src/model/nodelist';
 import Element from '../../../src/model/element';
+import DocumentFragment from '../../../src/model/documentfragment';
 import InsertOperation from '../../../src/model/operation/insertoperation';
 import RemoveOperation from '../../../src/model/operation/removeoperation';
 import Position from '../../../src/model/position';
@@ -13,10 +14,11 @@ import Text from '../../../src/model/text';
 import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
 
 describe( 'InsertOperation', () => {
-	let doc, root;
+	let model, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 		root = doc.createRoot();
 	} );
 
@@ -31,7 +33,7 @@ describe( 'InsertOperation', () => {
 	} );
 
 	it( 'should insert text node', () => {
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new InsertOperation(
 				new Position( root, [ 0 ] ),
 				new Text( 'x' ),
@@ -45,7 +47,7 @@ describe( 'InsertOperation', () => {
 	} );
 
 	it( 'should insert element', () => {
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new InsertOperation(
 				new Position( root, [ 0 ] ),
 				new Element( 'p' ),
@@ -59,7 +61,7 @@ describe( 'InsertOperation', () => {
 	} );
 
 	it( 'should insert set of nodes', () => {
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new InsertOperation(
 				new Position( root, [ 0 ] ),
 				[ 'bar', new Element( 'p' ), 'foo' ],
@@ -78,7 +80,7 @@ describe( 'InsertOperation', () => {
 	it( 'should insert between existing nodes', () => {
 		root.insertChildren( 0, new Text( 'xy' ) );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new InsertOperation(
 				new Position( root, [ 1 ] ),
 				'bar',
@@ -92,7 +94,7 @@ describe( 'InsertOperation', () => {
 	} );
 
 	it( 'should insert text', () => {
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new InsertOperation(
 				new Position( root, [ 0 ] ),
 				[ 'foo', new Text( 'x' ), 'bar' ],
@@ -130,11 +132,11 @@ describe( 'InsertOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		doc.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( wrapInDelta( operation ) );
 
 		expect( doc.version ).to.equal( 1 );
 
-		doc.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( wrapInDelta( reverse ) );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 0 );
@@ -149,11 +151,11 @@ describe( 'InsertOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		doc.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( wrapInDelta( operation ) );
 
 		expect( doc.version ).to.equal( 1 );
 
-		doc.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( wrapInDelta( reverse ) );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 0 );
@@ -190,11 +192,11 @@ describe( 'InsertOperation', () => {
 		const element = new Element( 'p', { key: 'value' } );
 
 		const op = new InsertOperation( new Position( root, [ 0 ] ), element, doc.version );
-		doc.applyOperation( wrapInDelta( op ) );
+		model.applyOperation( wrapInDelta( op ) );
 
 		const text = new Text( 'text' );
 		const op2 = new InsertOperation( new Position( root, [ 0, 0 ] ), text, doc.version );
-		doc.applyOperation( wrapInDelta( op2 ) );
+		model.applyOperation( wrapInDelta( op2 ) );
 
 		expect( op.nodes.getNode( 0 ) ).not.to.equal( element );
 		expect( op.nodes.getNode( 0 ).name ).to.equal( 'p' );
@@ -218,7 +220,7 @@ describe( 'InsertOperation', () => {
 		} );
 
 		it( 'should return false when element is inserted to document fragment', () => {
-			const docFrag = doc.batch().createDocumentFragment();
+			const docFrag = new DocumentFragment();
 
 			const op = new InsertOperation(
 				new Position( docFrag, [ 0 ] ),

+ 42 - 41
packages/ckeditor5-engine/tests/model/operation/markeroperation.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import Text from '../../../src/model/text';
 import Range from '../../../src/model/range';
 import MarkerOperation from '../../../src/model/operation/markeroperation';
@@ -14,22 +14,23 @@ function matchRange( range ) {
 }
 
 describe( 'MarkerOperation', () => {
-	let doc, root, range;
+	let model, doc, root, range;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 		root = doc.createRoot();
 		root.appendChildren( new Text( 'foo' ) );
 		range = Range.createFromParentsAndOffsets( root, 0, root, 0 );
 	} );
 
 	it( 'should have property type equal to "marker"', () => {
-		const op = new MarkerOperation( 'name', null, range, doc.markers, 0 );
+		const op = new MarkerOperation( 'name', null, range, model.markers, 0 );
 		expect( op.type ).to.equal( 'marker' );
 	} );
 
 	it( 'should add marker to document marker collection', () => {
-		sinon.spy( doc.markers, 'set' );
+		sinon.spy( model.markers, 'set' );
 		sinon.spy( doc, 'fire' );
 
 		doc.on( 'change', ( evt, type, changes ) => {
@@ -38,42 +39,42 @@ describe( 'MarkerOperation', () => {
 			expect( changes.type ).to.equal( 'set' );
 		} );
 
-		doc.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', null, range, doc.markers, doc.version )
+		model.applyOperation( wrapInDelta(
+			new MarkerOperation( 'name', null, range, model.markers, doc.version )
 		) );
 
 		expect( doc.version ).to.equal( 1 );
-		expect( doc.markers.set.calledWith( 'name', matchRange( range ) ) );
-		expect( doc.markers.get( 'name' ).getRange().isEqual( range ) ).to.be.true;
+		expect( model.markers.set.calledWith( 'name', matchRange( range ) ) );
+		expect( model.markers.get( 'name' ).getRange().isEqual( range ) ).to.be.true;
 		expect( doc.fire.called ).to.be.true;
 	} );
 
 	it( 'should update marker in document marker collection', () => {
-		doc.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', null, range, doc.markers, doc.version )
+		model.applyOperation( wrapInDelta(
+			new MarkerOperation( 'name', null, range, model.markers, doc.version )
 		) );
 
 		const range2 = Range.createFromParentsAndOffsets( root, 0, root, 3 );
 
-		sinon.spy( doc.markers, 'set' );
+		sinon.spy( model.markers, 'set' );
 		sinon.spy( doc, 'fire' );
 
-		doc.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', range, range2, doc.markers, doc.version )
+		model.applyOperation( wrapInDelta(
+			new MarkerOperation( 'name', range, range2, model.markers, doc.version )
 		) );
 
 		expect( doc.version ).to.equal( 2 );
-		expect( doc.markers.set.calledWith( 'name', matchRange( range2 ) ) );
-		expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true;
+		expect( model.markers.set.calledWith( 'name', matchRange( range2 ) ) );
+		expect( model.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true;
 		expect( doc.fire.called ).to.be.true;
 	} );
 
 	it( 'should remove marker from document marker collection', () => {
-		doc.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', null, range, doc.markers, doc.version )
+		model.applyOperation( wrapInDelta(
+			new MarkerOperation( 'name', null, range, model.markers, doc.version )
 		) );
 
-		sinon.spy( doc.markers, 'remove' );
+		sinon.spy( model.markers, 'remove' );
 		sinon.spy( doc, 'fire' );
 
 		doc.on( 'change', ( evt, type, changes ) => {
@@ -82,19 +83,19 @@ describe( 'MarkerOperation', () => {
 			expect( changes.type ).to.equal( 'remove' );
 		} );
 
-		doc.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', range, null, doc.markers, doc.version )
+		model.applyOperation( wrapInDelta(
+			new MarkerOperation( 'name', range, null, model.markers, doc.version )
 		) );
 
 		expect( doc.version ).to.equal( 2 );
-		expect( doc.markers.remove.calledWith( 'name' ) );
-		expect( doc.markers.get( 'name' ) ).to.be.null;
+		expect( model.markers.remove.calledWith( 'name' ) );
+		expect( model.markers.get( 'name' ) ).to.be.null;
 		expect( doc.fire.called ).to.be.true;
 	} );
 
 	it( 'should fire document change event but not document markers remove event if removing non-existing range', () => {
 		sinon.spy( doc, 'fire' );
-		sinon.spy( doc.markers, 'fire' );
+		sinon.spy( model.markers, 'fire' );
 
 		doc.on( 'change', ( evt, type, changes ) => {
 			expect( type ).to.equal( 'marker' );
@@ -102,19 +103,19 @@ describe( 'MarkerOperation', () => {
 			expect( changes.type ).to.equal( 'remove' );
 		} );
 
-		doc.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', null, null, doc.markers, doc.version )
+		model.applyOperation( wrapInDelta(
+			new MarkerOperation( 'name', null, null, model.markers, doc.version )
 		) );
 
 		expect( doc.fire.calledWith( 'change', 'marker' ) ).to.be.true;
-		expect( doc.markers.fire.notCalled ).to.be.true;
+		expect( model.markers.fire.notCalled ).to.be.true;
 	} );
 
 	it( 'should fire document change event but not document markers set event if newRange is same as current marker range', () => {
-		doc.markers.set( 'name', range );
+		model.markers.set( 'name', range );
 
 		sinon.spy( doc, 'fire' );
-		sinon.spy( doc.markers, 'fire' );
+		sinon.spy( model.markers, 'fire' );
 
 		doc.on( 'change', ( evt, type, changes ) => {
 			expect( type ).to.equal( 'marker' );
@@ -122,21 +123,21 @@ describe( 'MarkerOperation', () => {
 			expect( changes.type ).to.equal( 'set' );
 		} );
 
-		doc.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', range, range, doc.markers, doc.version )
+		model.applyOperation( wrapInDelta(
+			new MarkerOperation( 'name', range, range, model.markers, doc.version )
 		) );
 
 		expect( doc.fire.calledWith( 'change', 'marker' ) ).to.be.true;
-		expect( doc.markers.fire.notCalled ).to.be.true;
+		expect( model.markers.fire.notCalled ).to.be.true;
 	} );
 
 	it( 'should return MarkerOperation with swapped ranges as reverse operation', () => {
 		const range2 = Range.createFromParentsAndOffsets( root, 0, root, 3 );
 
-		const op1 = new MarkerOperation( 'name', null, range, doc.markers, doc.version );
+		const op1 = new MarkerOperation( 'name', null, range, model.markers, doc.version );
 		const reversed1 = op1.getReversed();
 
-		const op2 = new MarkerOperation( 'name', range, range2, doc.markers, doc.version );
+		const op2 = new MarkerOperation( 'name', range, range2, model.markers, doc.version );
 		const reversed2 = op2.getReversed();
 
 		expect( reversed1 ).to.be.an.instanceof( MarkerOperation );
@@ -154,7 +155,7 @@ describe( 'MarkerOperation', () => {
 	} );
 
 	it( 'should create a MarkerOperation with the same parameters when cloned', () => {
-		const op = new MarkerOperation( 'name', null, range, doc.markers, 0 );
+		const op = new MarkerOperation( 'name', null, range, model.markers, 0 );
 		const clone = op.clone();
 
 		expect( clone ).to.be.an.instanceof( MarkerOperation );
@@ -163,19 +164,19 @@ describe( 'MarkerOperation', () => {
 
 	describe( 'isDocumentOperation', () => {
 		it( 'should return true when new marker range is added to the document', () => {
-			const op = new MarkerOperation( 'name', null, range, doc.markers, doc.version );
+			const op = new MarkerOperation( 'name', null, range, model.markers, doc.version );
 
 			expect( op.isDocumentOperation ).to.true;
 		} );
 
 		it( 'should return false when marker range is removed from the document', () => {
-			const op = new MarkerOperation( 'name', range, null, doc.markers, doc.version );
+			const op = new MarkerOperation( 'name', range, null, model.markers, doc.version );
 
 			expect( op.isDocumentOperation ).to.true;
 		} );
 
 		it( 'should return true when non-existing marker range is removed from the document', () => {
-			const op = new MarkerOperation( 'name', null, null, doc.markers, doc.version );
+			const op = new MarkerOperation( 'name', null, null, model.markers, doc.version );
 
 			expect( op.isDocumentOperation ).to.true;
 		} );
@@ -183,7 +184,7 @@ describe( 'MarkerOperation', () => {
 
 	describe( 'toJSON', () => {
 		it( 'should create proper serialized object', () => {
-			const op = new MarkerOperation( 'name', null, range, doc.markers, doc.version );
+			const op = new MarkerOperation( 'name', null, range, model.markers, doc.version );
 			const serialized = jsonParseStringify( op );
 
 			expect( serialized ).to.deep.equal( {
@@ -198,7 +199,7 @@ describe( 'MarkerOperation', () => {
 
 	describe( 'fromJSON', () => {
 		it( 'should create proper MarkerOperation from json object #1', () => {
-			const op = new MarkerOperation( 'name', null, range, doc.markers, doc.version );
+			const op = new MarkerOperation( 'name', null, range, model.markers, doc.version );
 
 			const serialized = jsonParseStringify( op );
 			const deserialized = MarkerOperation.fromJSON( serialized, doc );
@@ -208,7 +209,7 @@ describe( 'MarkerOperation', () => {
 
 		it( 'should create proper MarkerOperation from json object #2', () => {
 			// Gotta love 100% CC.
-			const op = new MarkerOperation( 'name', range, null, doc.markers, doc.version );
+			const op = new MarkerOperation( 'name', range, null, model.markers, doc.version );
 
 			const serialized = jsonParseStringify( op );
 			const deserialized = MarkerOperation.fromJSON( serialized, doc );

+ 17 - 17
packages/ckeditor5-engine/tests/model/operation/moveoperation.js

@@ -3,19 +3,21 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import MoveOperation from '../../../src/model/operation/moveoperation';
 import Position from '../../../src/model/position';
+import DocumentFragment from '../../../src/model/documentfragment';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
 
 describe( 'MoveOperation', () => {
-	let doc, root;
+	let model, doc, root;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 		root = doc.createRoot();
 	} );
 
@@ -47,7 +49,7 @@ describe( 'MoveOperation', () => {
 
 		root.insertChildren( 0, [ p1, p2 ] );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new MoveOperation(
 				new Position( root, [ 0, 0 ] ),
 				1,
@@ -68,7 +70,7 @@ describe( 'MoveOperation', () => {
 	it( 'should move position of children in one node backward', () => {
 		root.insertChildren( 0, new Text( 'xbarx' ) );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new MoveOperation(
 				new Position( root, [ 2 ] ),
 				2,
@@ -85,7 +87,7 @@ describe( 'MoveOperation', () => {
 	it( 'should move position of children in one node forward', () => {
 		root.insertChildren( 0, new Text( 'xbarx' ) );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new MoveOperation(
 				new Position( root, [ 1 ] ),
 				2,
@@ -132,7 +134,7 @@ describe( 'MoveOperation', () => {
 			doc.version
 		);
 
-		doc.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( wrapInDelta( operation ) );
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 2 );
@@ -140,7 +142,7 @@ describe( 'MoveOperation', () => {
 		expect( p2.maxOffset ).to.equal( 1 );
 		expect( p2.getChild( 0 ).name ).to.equal( 'x' );
 
-		doc.applyOperation( wrapInDelta( operation.getReversed() ) );
+		model.applyOperation( wrapInDelta( operation.getReversed() ) );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 2 );
@@ -159,7 +161,7 @@ describe( 'MoveOperation', () => {
 			doc.version
 		);
 
-		expect( () => doc.applyOperation( wrapInDelta( operation ) ) ).to.throw( CKEditorError, /move-operation-nodes-do-not-exist/ );
+		expect( () => model.applyOperation( wrapInDelta( operation ) ) ).to.throw( CKEditorError, /move-operation-nodes-do-not-exist/ );
 	} );
 
 	it( 'should throw an error if target or source parent-element specified by position does not exist', () => {
@@ -176,7 +178,7 @@ describe( 'MoveOperation', () => {
 
 		root.removeChildren( 1 );
 
-		expect( () => doc.applyOperation( wrapInDelta( operation ) ) ).to.throw( CKEditorError, /move-operation-position-invalid/ );
+		expect( () => model.applyOperation( wrapInDelta( operation ) ) ).to.throw( CKEditorError, /move-operation-position-invalid/ );
 	} );
 
 	it( 'should throw an error if operation tries to move a range between the beginning and the end of that range', () => {
@@ -189,7 +191,7 @@ describe( 'MoveOperation', () => {
 			doc.version
 		);
 
-		expect( () => doc.applyOperation( wrapInDelta( operation ) ) ).to.throw( CKEditorError, /move-operation-range-into-itself/ );
+		expect( () => model.applyOperation( wrapInDelta( operation ) ) ).to.throw( CKEditorError, /move-operation-range-into-itself/ );
 	} );
 
 	it( 'should throw an error if operation tries to move a range into a sub-tree of a node that is in that range', () => {
@@ -203,7 +205,7 @@ describe( 'MoveOperation', () => {
 			doc.version
 		);
 
-		expect( () => doc.applyOperation( wrapInDelta( operation ) ) ).to.throw( CKEditorError, /move-operation-node-into-itself/ );
+		expect( () => model.applyOperation( wrapInDelta( operation ) ) ).to.throw( CKEditorError, /move-operation-node-into-itself/ );
 	} );
 
 	it( 'should not throw an error if operation move a range into a sibling', () => {
@@ -219,7 +221,7 @@ describe( 'MoveOperation', () => {
 
 		expect(
 			() => {
-				doc.applyOperation( wrapInDelta( operation ) );
+				model.applyOperation( wrapInDelta( operation ) );
 			}
 		).not.to.throw();
 
@@ -242,7 +244,7 @@ describe( 'MoveOperation', () => {
 
 		expect(
 			() => {
-				doc.applyOperation( wrapInDelta( operation ) );
+				model.applyOperation( wrapInDelta( operation ) );
 			}
 		).not.to.throw();
 	} );
@@ -280,9 +282,7 @@ describe( 'MoveOperation', () => {
 		} );
 
 		it( 'should return false when operation is executed on detached items', () => {
-			const docFrag = doc.batch().createDocumentFragment();
-
-			doc.batch().appendText( 'abc', null, docFrag );
+			const docFrag = new DocumentFragment( [ new Text( 'abc' ) ] );
 
 			const op = new MoveOperation(
 				new Position( docFrag, [ 0 ] ),

+ 5 - 4
packages/ckeditor5-engine/tests/model/operation/nooperation.js

@@ -3,20 +3,21 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import NoOperation from '../../../src/model/operation/nooperation';
 import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
 
 describe( 'NoOperation', () => {
-	let noop, doc;
+	let model, noop, doc;
 
 	beforeEach( () => {
 		noop = new NoOperation( 0 );
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 	} );
 
 	it( 'should not throw an error when applied', () => {
-		expect( () => doc.applyOperation( wrapInDelta( noop ) ) ).to.not.throw( Error );
+		expect( () => model.applyOperation( wrapInDelta( noop ) ) ).to.not.throw( Error );
 	} );
 
 	it( 'should return empty object when executed', () => {

+ 4 - 4
packages/ckeditor5-engine/tests/model/operation/operationfactory.js

@@ -3,22 +3,22 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import NoOperation from '../../../src/model/operation/nooperation';
 import OperationFactory from '../../../src/model/operation/operationfactory';
 
 describe( 'OperationFactory', () => {
-	let doc;
+	let model;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
 	} );
 
 	it( 'should create operation from JSON', () => {
 		const operation = OperationFactory.fromJSON( {
 			__className: 'engine.model.operation.NoOperation',
 			baseVersion: 0
-		}, doc );
+		}, model.doc );
 
 		expect( operation ).to.instanceof( NoOperation );
 		expect( operation.baseVersion ).to.equal( 0 );

+ 9 - 7
packages/ckeditor5-engine/tests/model/operation/reinsertoperation.js

@@ -3,21 +3,23 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import ReinsertOperation from '../../../src/model/operation/reinsertoperation';
 import RemoveOperation from '../../../src/model/operation/removeoperation';
 import MoveOperation from '../../../src/model/operation/moveoperation';
 import Position from '../../../src/model/position';
+import DocumentFragment from '../../../src/model/documentfragment';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
 
 describe( 'ReinsertOperation', () => {
-	let doc, root, graveyard, operation, graveyardPosition, rootPosition;
+	let model, doc, root, graveyard, operation, graveyardPosition, rootPosition;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 		root = doc.createRoot();
 		graveyard = doc.graveyard;
 
@@ -87,13 +89,13 @@ describe( 'ReinsertOperation', () => {
 
 		graveyard.insertChildren( 0, new Text( 'xx' ) );
 
-		doc.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( wrapInDelta( operation ) );
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 2 );
 		expect( graveyard.maxOffset ).to.equal( 0 );
 
-		doc.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( wrapInDelta( reverse ) );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 0 );
@@ -105,7 +107,7 @@ describe( 'ReinsertOperation', () => {
 	} );
 
 	it( 'should throw when target position is not in the document', () => {
-		const docFrag = doc.batch().createDocumentFragment();
+		const docFrag = new DocumentFragment();
 
 		operation = new ReinsertOperation(
 			graveyardPosition,
@@ -120,7 +122,7 @@ describe( 'ReinsertOperation', () => {
 	} );
 
 	it( 'should throw when source position is not in the document', () => {
-		const docFrag = doc.batch().createDocumentFragment();
+		const docFrag = new DocumentFragment();
 
 		operation = new ReinsertOperation(
 			Position.createAt( docFrag ),

+ 13 - 12
packages/ckeditor5-engine/tests/model/operation/removeoperation.js

@@ -3,21 +3,23 @@
  * For licensing, see LICENSE.md.
  */
 
-import Document from '../../../src/model/document';
+import Model from '../../../src/model/model';
 import ReinsertOperation from '../../../src/model/operation/reinsertoperation';
 import RemoveOperation from '../../../src/model/operation/removeoperation';
 import MoveOperation from '../../../src/model/operation/moveoperation';
 import Position from '../../../src/model/position';
-import Text from '../../../src/model/text';
+import DocumentFragment from '../../../src/model/documentfragment';
 import Element from '../../../src/model/element';
+import Text from '../../../src/model/text';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
 
 describe( 'RemoveOperation', () => {
-	let doc, root, graveyard;
+	let model, doc, root, graveyard;
 
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 		root = doc.createRoot();
 		graveyard = doc.graveyard;
 	} );
@@ -58,7 +60,7 @@ describe( 'RemoveOperation', () => {
 	it( 'should be able to remove set of nodes and append them to graveyard root', () => {
 		root.insertChildren( 0, new Text( 'fozbar' ) );
 
-		doc.applyOperation( wrapInDelta(
+		model.applyOperation( wrapInDelta(
 			new RemoveOperation(
 				new Position( root, [ 2 ] ),
 				2,
@@ -115,12 +117,12 @@ describe( 'RemoveOperation', () => {
 
 		root.insertChildren( 0, new Text( 'bar' ) );
 
-		doc.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( wrapInDelta( operation ) );
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 0 );
 
-		doc.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( wrapInDelta( reverse ) );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 3 );
@@ -133,7 +135,7 @@ describe( 'RemoveOperation', () => {
 		const position = new Position( doc.graveyard, [ 2 ] );
 		const operation = new RemoveOperation( position, 1, new Position( doc.graveyard, [ 0 ] ), 0 );
 
-		doc.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( wrapInDelta( operation ) );
 
 		expect( doc.graveyard.childCount ).to.equal( 3 );
 		expect( doc.graveyard.getChild( 0 ).name ).to.equal( 'z' );
@@ -142,11 +144,10 @@ describe( 'RemoveOperation', () => {
 	} );
 
 	it( 'should throw when is executed on detached item', () => {
-		const batch = doc.batch();
-		const docFrag = batch.createDocumentFragment();
-		const item = batch.createElement( 'foo' );
+		const docFrag = new DocumentFragment();
+		const item = new Element( 'foo' );
 
-		batch.append( item, docFrag );
+		docFrag.appendChildren( [ item ] );
 
 		const op = new RemoveOperation(
 			new Position( docFrag, [ 0 ] ),

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików