Explorar el Código

Changed: PR fixes for Markers.

Szymon Cofalik hace 9 años
padre
commit
051c792935

+ 13 - 30
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -8,10 +8,16 @@
  */
  */
 
 
 import ViewDocument from '../view/document.js';
 import ViewDocument from '../view/document.js';
-import ModelRange from '../model/range.js';
 import Mapper from '../conversion/mapper.js';
 import Mapper from '../conversion/mapper.js';
 import ModelConversionDispatcher from '../conversion/modelconversiondispatcher.js';
 import ModelConversionDispatcher from '../conversion/modelconversiondispatcher.js';
-import { insertText, remove, move, rename } from '../conversion/model-to-view-converters.js';
+import {
+	insertText,
+	remove,
+	move,
+	rename,
+	insertIntoMarker,
+	moveInOutOfMarker
+} from '../conversion/model-to-view-converters.js';
 import { convertSelectionChange } from '../conversion/view-selection-to-model-converters.js';
 import { convertSelectionChange } from '../conversion/view-selection-to-model-converters.js';
 import {
 import {
 	convertRangeSelection,
 	convertRangeSelection,
@@ -99,11 +105,11 @@ export default class EditingController {
 		}, { priority: 'low' } );
 		}, { priority: 'low' } );
 
 
 		// Convert model markers changes.
 		// Convert model markers changes.
-		this._listener.listenTo( this.model.markers, 'addMarker', ( evt, name, range ) => {
+		this._listener.listenTo( this.model.markers, 'add', ( evt, name, range ) => {
 			this.modelToView.convertMarker( 'addMarker', name, range );
 			this.modelToView.convertMarker( 'addMarker', name, range );
 		} );
 		} );
 
 
-		this._listener.listenTo( this.model.markers, 'removeMarker', ( evt, name, range ) => {
+		this._listener.listenTo( this.model.markers, 'remove', ( evt, name, range ) => {
 			this.modelToView.convertMarker( 'removeMarker', name, range );
 			this.modelToView.convertMarker( 'removeMarker', name, range );
 		} );
 		} );
 
 
@@ -116,32 +122,9 @@ export default class EditingController {
 		this.modelToView.on( 'move', move(), { priority: 'low' } );
 		this.modelToView.on( 'move', move(), { priority: 'low' } );
 		this.modelToView.on( 'rename', rename(), { priority: 'low' } );
 		this.modelToView.on( 'rename', rename(), { priority: 'low' } );
 
 
-		// Attach markers converters.
-		this.modelToView.on( 'insert', ( evt, data ) => {
-			const pos = data.range.start;
-
-			for ( let [ range, name ] of this.model.markers._markerNames ) {
-				if ( range.containsPosition( pos ) || range.start.isEqual( pos ) ) {
-					this.modelToView.convertMarker( 'addMarker', name, data.range );
-				}
-			}
-		}, { priority: 'lowest' } );
-
-		this.modelToView.on( 'move', ( evt, data ) => {
-			const pos = data.sourcePosition._getTransformedByInsertion( data.targetPosition, data.item.offsetSize );
-
-			for ( let [ range, name ] of this.model.markers._markerNames ) {
-				if ( range.start.isEqual( pos ) || range.end.isEqual( pos ) ) {
-					const movedRange = ModelRange.createOn( data.item );
-
-					if ( range.containsPosition( data.targetPosition ) ) {
-						this.modelToView.convertMarker( 'addMarker', name, movedRange );
-					} else {
-						this.modelToView.convertMarker( 'removeMarker', name, movedRange );
-					}
-				}
-			}
-		}, { priority: 'lowest' } );
+		// Attach default markers converters.
+		this.modelToView.on( 'insert', insertIntoMarker( this.model.markers ), { priority: 'lowest' } );
+		this.modelToView.on( 'move', moveInOutOfMarker( this.model.markers ), { priority: 'lowest' } );
 
 
 		// Attach default selection converters.
 		// Attach default selection converters.
 		this.modelToView.on( 'selection', clearAttributes(), { priority: 'low' } );
 		this.modelToView.on( 'selection', clearAttributes(), { priority: 'low' } );

+ 19 - 11
packages/ckeditor5-engine/src/conversion/buildmodelconverter.js

@@ -11,10 +11,10 @@ import {
 	insertElement,
 	insertElement,
 	setAttribute,
 	setAttribute,
 	removeAttribute,
 	removeAttribute,
-	wrap,
-	unwrap,
-	wrapMarker,
-	unwrapMarker
+	wrapItem,
+	unwrapItem,
+	wrapRange,
+	unwrapRange
 } from './model-to-view-converters.js';
 } from './model-to-view-converters.js';
 
 
 import { convertSelectionAttribute } from './model-selection-to-view-converters.js';
 import { convertSelectionAttribute } from './model-selection-to-view-converters.js';
@@ -22,6 +22,8 @@ import { convertSelectionAttribute } from './model-selection-to-view-converters.
 import ViewAttributeElement from '../view/attributeelement.js';
 import ViewAttributeElement from '../view/attributeelement.js';
 import ViewContainerElement from '../view/containerelement.js';
 import ViewContainerElement from '../view/containerelement.js';
 
 
+import CKEditorError from '../../utils/ckeditorerror.js';
+
 /**
 /**
  * Provides chainable, high-level API to easily build basic model-to-view converters that are appended to given
  * Provides chainable, high-level API to easily build basic model-to-view converters that are appended to given
  * dispatchers. In many cases, this is the API that should be used to specify how abstract model elements and
  * dispatchers. In many cases, this is the API that should be used to specify how abstract model elements and
@@ -233,17 +235,17 @@ class ModelConverterBuilder {
 				// From model attribute to view element -> wrap and unwrap.
 				// From model attribute to view element -> wrap and unwrap.
 				element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
 				element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
 
 
-				dispatcher.on( 'addAttribute:' + this._from.key, wrap( element ), { priority } );
-				dispatcher.on( 'changeAttribute:' + this._from.key, wrap( element ), { priority } );
-				dispatcher.on( 'removeAttribute:' + this._from.key, unwrap( element ), { priority } );
+				dispatcher.on( 'addAttribute:' + this._from.key, wrapItem( element ), { priority } );
+				dispatcher.on( 'changeAttribute:' + this._from.key, wrapItem( element ), { priority } );
+				dispatcher.on( 'removeAttribute:' + this._from.key, unwrapItem( element ), { priority } );
 
 
 				dispatcher.on( 'selectionAttribute:' + this._from.key, convertSelectionAttribute( element ), { priority } );
 				dispatcher.on( 'selectionAttribute:' + this._from.key, convertSelectionAttribute( element ), { priority } );
 			} else {
 			} else {
 				// From marker to view element -> wrapRange and unwrapRange.
 				// From marker to view element -> wrapRange and unwrapRange.
 				element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
 				element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
 
 
-				dispatcher.on( 'addMarker:' + this._from.name, wrapMarker( element ), { priority } );
-				dispatcher.on( 'removeMarker:' + this._from.name, unwrapMarker( element ), { priority } );
+				dispatcher.on( 'addMarker:' + this._from.name, wrapRange( element ), { priority } );
+				dispatcher.on( 'removeMarker:' + this._from.name, unwrapRange( element ), { priority } );
 			}
 			}
 		}
 		}
 	}
 	}
@@ -281,8 +283,14 @@ class ModelConverterBuilder {
 	 */
 	 */
 	toAttribute( keyOrCreator, value ) {
 	toAttribute( keyOrCreator, value ) {
 		if ( this._from.type != 'attribute' ) {
 		if ( this._from.type != 'attribute' ) {
-			// Only converting from attribute to attribute is supported.
-			return;
+			/**
+			 * To-attribute conversion is supported only for model attributes.
+			 *
+			 * @error build-model-converter-element-to-attribute
+			 * @param {module:engine/model/range~Range} range
+			 */
+			throw new CKEditorError( 'build-model-converter-non-attribute-to-attribute: ' +
+				'To-attribute conversion is supported only from model attributes.' );
 		}
 		}
 
 
 		let attributeCreator;
 		let attributeCreator;

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

@@ -10,9 +10,7 @@ import ModelElement from '../model/element.js';
 import ViewRange from '../view/range.js';
 import ViewRange from '../view/range.js';
 import ViewPosition from '../view/position.js';
 import ViewPosition from '../view/position.js';
 import ViewElement from '../view/element.js';
 import ViewElement from '../view/element.js';
-import ViewContainerElement from '../view/containerelement.js';
 import ViewText from '../view/text.js';
 import ViewText from '../view/text.js';
-import ViewTreeWalker from '../view/treewalker.js';
 import viewWriter from '../view/writer.js';
 import viewWriter from '../view/writer.js';
 
 
 /**
 /**
@@ -203,13 +201,13 @@ export function removeAttribute( attributeCreator ) {
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
  * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
  *
  *
- *		modelDispatcher.on( 'addAttribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
+ *		modelDispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
  *
  *
  * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
  * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
  * be used for wrapping.
  * be used for wrapping.
  * @returns {Function} Set/change attribute converter.
  * @returns {Function} Set/change attribute converter.
  */
  */
-export function wrap( elementCreator ) {
+export function wrapItem( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
 	return ( evt, data, consumable, conversionApi ) => {
 		let viewRange = conversionApi.mapper.toViewRange( data.range );
 		let viewRange = conversionApi.mapper.toViewRange( data.range );
 
 
@@ -250,14 +248,14 @@ export function wrap( elementCreator ) {
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
  * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
  *
  *
- *		modelDispatcher.on( 'removeAttribute:bold', unwrap( new ViewAttributeElement( 'strong' ) ) );
+ *		modelDispatcher.on( 'removeAttribute:bold', unwrapItem( new ViewAttributeElement( 'strong' ) ) );
  *
  *
- * @see module:engine/conversion/model-to-view-converters~wrap
+ * @see module:engine/conversion/model-to-view-converters~wrapItem
  * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
  * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
  * be used for unwrapping.
  * be used for unwrapping.
  * @returns {Function} Remove attribute converter.
  * @returns {Function} Remove attribute converter.
  */
  */
-export function unwrap( elementCreator ) {
+export function unwrapItem( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
 	return ( evt, data, consumable, conversionApi ) => {
 		if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
 		if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
 			return;
 			return;
@@ -292,15 +290,13 @@ export function unwrap( elementCreator ) {
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * {@link engine.conversion.ModelConversionDispatcher}).
  * {@link engine.conversion.ModelConversionDispatcher}).
  *
  *
- *		modelDispatcher.on( 'addMarker:searchResult', wrapMarker( new ViewAttributeElement( 'span', { class: 'searchResult' } ) ) );
+ *		modelDispatcher.on( 'addMarker:searchResult', wrapRange( new ViewAttributeElement( 'span', { class: 'searchResult' } ) ) );
  *
  *
- * @external engine.conversion.modelToView
- * @function engine.conversion.modelToView.wrapMarker
  * @param {engine.view.AttributeElement|Function} elementCreator View attribute element, or function returning
  * @param {engine.view.AttributeElement|Function} elementCreator View attribute element, or function returning
  * a view attribute element, which will be used for wrapping.
  * a view attribute element, which will be used for wrapping.
- * @returns {Function} Add marker converter.
+ * @returns {Function} Wrap range converter.
  */
  */
-export function wrapMarker( elementCreator ) {
+export function wrapRange( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
 	return ( evt, data, consumable, conversionApi ) => {
 		const viewRange = conversionApi.mapper.toViewRange( data.range );
 		const viewRange = conversionApi.mapper.toViewRange( data.range );
 
 
@@ -309,14 +305,14 @@ export function wrapMarker( elementCreator ) {
 			elementCreator( data, consumable, conversionApi );
 			elementCreator( data, consumable, conversionApi );
 
 
 		if ( viewElement ) {
 		if ( viewElement ) {
-			if ( !consumable.consume( data.range, 'range' ) ) {
+			if ( !consumable.consume( data.range, 'addMarker' ) ) {
 				return;
 				return;
 			}
 			}
 
 
 			if ( viewRange.isCollapsed ) {
 			if ( viewRange.isCollapsed ) {
 				viewWriter.wrapPosition( viewRange.start, viewElement );
 				viewWriter.wrapPosition( viewRange.start, viewElement );
 			} else {
 			} else {
-				const flatViewRanges = breakViewRangeOnFlat( viewRange );
+				const flatViewRanges = viewWriter.breakViewRangePerContainer( viewRange );
 
 
 				for ( let range of flatViewRanges ) {
 				for ( let range of flatViewRanges ) {
 					viewWriter.wrap( range, viewElement );
 					viewWriter.wrap( range, viewElement );
@@ -339,18 +335,16 @@ export function wrapMarker( elementCreator ) {
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * {@link engine.conversion.ModelConversionDispatcher}) and bind model and view elements.
  * {@link engine.conversion.ModelConversionDispatcher}) and bind model and view elements.
  *
  *
- *		modelDispatcher.on( 'removeMarker:searchResult', unwrapMarker( new ViewAttributeElement( 'span', { class: 'searchResult' } ) ) );
+ *		modelDispatcher.on( 'removeMarker:searchResult', unwrapRange( new ViewAttributeElement( 'span', { class: 'searchResult' } ) ) );
  *
  *
- * @see engine.conversion.modelToView.wrapMarker
- * @external engine.conversion.modelToView
- * @function engine.conversion.modelToView.unwrapMarker
+ * @see engine.conversion.modelToView.wrapRange
  * @param {engine.view.AttributeElement|Function} elementCreator View attribute element, or function returning
  * @param {engine.view.AttributeElement|Function} elementCreator View attribute element, or function returning
  * a view attribute element, which will be used for unwrapping.
  * a view attribute element, which will be used for unwrapping.
- * @returns {Function} Remove attribute converter.
+ * @returns {Function} Unwrap range converter.
  */
  */
-export function unwrapMarker( elementCreator ) {
+export function unwrapRange( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
 	return ( evt, data, consumable, conversionApi ) => {
-		if ( !consumable.consume( data.range, 'range' ) ) {
+		if ( !consumable.consume( data.range, 'removeMarker' ) ) {
 			return;
 			return;
 		}
 		}
 
 
@@ -366,12 +360,12 @@ export function unwrapMarker( elementCreator ) {
 				viewStart = ViewPosition.createAfter( viewStart.parent );
 				viewStart = ViewPosition.createAfter( viewStart.parent );
 			}
 			}
 
 
-			const viewEnd = findFurthestZeroLengthPoint( viewStart, conversionApi.mapper );
+			const viewRange = enlargeViewPosition( viewStart, conversionApi.mapper );
 
 
-			viewWriter.unwrap( new ViewRange( viewStart, viewEnd ), viewElement );
+			viewWriter.unwrap( viewRange, viewElement );
 		} else {
 		} else {
 			const viewRange = conversionApi.mapper.toViewRange( data.range );
 			const viewRange = conversionApi.mapper.toViewRange( data.range );
-			const flatViewRanges = breakViewRangeOnFlat( viewRange );
+			const flatViewRanges = viewWriter.breakViewRangePerContainer( viewRange );
 
 
 			for ( let range of flatViewRanges ) {
 			for ( let range of flatViewRanges ) {
 				viewWriter.unwrap( range, viewElement );
 				viewWriter.unwrap( range, viewElement );
@@ -380,47 +374,20 @@ export function unwrapMarker( elementCreator ) {
 	};
 	};
 }
 }
 
 
-// Starting from given `viewPosition`, moving forward, finds the furthest position in a view, which is
-// separated only by elements with zero model length (which in most cases are empty `ViewAttributeElement`s).
-// Using `viewPosition` and returned position, one can build a view range, which start and position are mapped to
-// the same position in model.
+// Takes given `viewPosition` and returns a widest possible range that contains this position and all view elements
+// before that position and after that position which has zero length in model (in most cases empty `ViewAttributeElement`s).
 // @param {engine.view.Position} viewPosition Position to start from when looking for furthest zero length position.
 // @param {engine.view.Position} viewPosition Position to start from when looking for furthest zero length position.
 // @param {engine.conversion.Mapper} mapper Mapper to use when looking for furthest zero length position.
 // @param {engine.conversion.Mapper} mapper Mapper to use when looking for furthest zero length position.
-// @returns {engine.view.Position} Furthest zero length position.
-function findFurthestZeroLengthPoint( viewPosition, mapper ) {
-	viewPosition = ViewPosition.createFromPosition( viewPosition );
+// @returns {engine.view.Range}
+function enlargeViewPosition( viewPosition, mapper ) {
+	const start = ViewPosition.createFromPosition( viewPosition );
+	const end = ViewPosition.createFromPosition( viewPosition );
 
 
-	while ( viewPosition.nodeAfter && mapper.getModelLength( viewPosition.nodeAfter ) === 0 ) {
-		viewPosition.offset++;
+	while ( end.nodeAfter && mapper.getModelLength( end.nodeAfter ) === 0 ) {
+		end.offset++;
 	}
 	}
 
 
-	return viewPosition;
-}
-
-// Breaks given `viewRange` on "flat" view ranges, where "flat" means that the range is contained within
-// one container element. After `viewRange` is broken, it's "pieces" can be used in ViewWriter (which expects
-// that passed ranges are contained within one container element).
-// @param {engine.view.Range} viewRange Range to break.
-// @returns {Array.<engine.view.Range>} Flat ranges that combines into passed `viewRange`.
-function breakViewRangeOnFlat( viewRange ) {
-	const ranges = [];
-	const walker = new ViewTreeWalker( { boundaries: viewRange } );
-
-	let start = viewRange.start;
-
-	for ( let value of walker ) {
-		if ( value.item instanceof ViewContainerElement ) {
-			if ( !start.isEqual( value.previousPosition ) ) {
-				ranges.push( new ViewRange( start, value.previousPosition ) );
-			}
-
-			start = value.nextPosition;
-		}
-	}
-
-	ranges.push( new ViewRange( start, viewRange.end ) );
-
-	return ranges;
+	return new ViewRange( start, end );
 }
 }
 
 
 /**
 /**
@@ -517,6 +484,53 @@ export function rename() {
 	};
 	};
 }
 }
 
 
+/**
+ * Function factory, creates a default converter for inserting {@link module:engine/model/item~Item model item} into a marker range.
+ *
+ *		modelDispatcher.on( 'insert', insertIntoRange( modelDocument.markers ) );
+ *
+ * @param {module:engine/model/markerscollection~MarkersCollection} markersCollection Markers collection to check when
+ * inserting.
+ * @returns {Function}
+ */
+export function insertIntoMarker( markersCollection ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		for ( let [ name, range ] of markersCollection ) {
+			if ( range.containsPosition( data.range.start ) ) {
+				conversionApi.dispatcher.convertMarker( 'addMarker', name, data.range );
+			}
+		}
+	};
+}
+
+/**
+ * Function factory, creates a default converter for move changes that move {@link module:engine/model/item~Item model items}
+ * into or outside of a marker range.
+ *
+ *		modelDispatcher.on( 'move', moveInOutOfMarker( modelDocument.markers ) );
+ *
+ * @param {module:engine/model/markerscollection~MarkersCollection} markersCollection Markers collection to check when
+ * moving.
+ * @returns {Function}
+ */
+export function moveInOutOfMarker( markersCollection ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		const sourcePos = data.sourcePosition._getTransformedByInsertion( data.targetPosition, data.item.offsetSize );
+		const movedRange = ModelRange.createOn( data.item );
+
+		for ( let [ name, range ] of markersCollection ) {
+			const wasInMarker = range.containsPosition( sourcePos ) || range.start.isEqual( sourcePos ) || range.end.isEqual( sourcePos );
+			const isInMarker = range.containsPosition( movedRange.start );
+
+			if ( wasInMarker && !isInMarker ) {
+				conversionApi.dispatcher.convertMarker( 'removeMarker', name, movedRange );
+			} else if ( !wasInMarker && isInMarker ) {
+				conversionApi.dispatcher.convertMarker( 'addMarker', name, movedRange );
+			}
+		}
+	};
+}
+
 /**
 /**
  * Returns the consumable type that is to be consumed in an event, basing on that event name.
  * Returns the consumable type that is to be consumed in an event, basing on that event name.
  *
  *

+ 58 - 8
packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js

@@ -213,7 +213,7 @@ export default class ModelConversionDispatcher {
 	 */
 	 */
 	convertMove( sourcePosition, range ) {
 	convertMove( sourcePosition, range ) {
 		// Keep in mind that move dispatcher expects flat range.
 		// Keep in mind that move dispatcher expects flat range.
-		const consumable = this._createConsumableForType( range, 'move' );
+		const consumable = this._createConsumableForRange( range, 'move' );
 
 
 		const items = Array.from( range.getItems( { shallow: true } ) );
 		const items = Array.from( range.getItems( { shallow: true } ) );
 		const inSameParent = sourcePosition.parent == range.start.parent;
 		const inSameParent = sourcePosition.parent == range.start.parent;
@@ -243,7 +243,7 @@ export default class ModelConversionDispatcher {
 	 * graveyard root}).
 	 * graveyard root}).
 	 */
 	 */
 	convertRemove( sourcePosition, range ) {
 	convertRemove( sourcePosition, range ) {
-		const consumable = this._createConsumableForType( range, 'remove' );
+		const consumable = this._createConsumableForRange( range, 'remove' );
 
 
 		for ( let item of range.getItems( { shallow: true } ) ) {
 		for ( let item of range.getItems( { shallow: true } ) ) {
 			const data = {
 			const data = {
@@ -269,7 +269,7 @@ export default class ModelConversionDispatcher {
 	 */
 	 */
 	convertAttribute( type, range, key, oldValue, newValue ) {
 	convertAttribute( type, range, key, oldValue, newValue ) {
 		// Create a list with attributes to consume.
 		// Create a list with attributes to consume.
-		const consumable = this._createConsumableForType( range, type + ':' + key );
+		const consumable = this._createConsumableForRange( range, type + ':' + key );
 
 
 		// Create a separate attribute event for each node in the range.
 		// Create a separate attribute event for each node in the range.
 		for ( let value of range ) {
 		for ( let value of range ) {
@@ -328,8 +328,17 @@ export default class ModelConversionDispatcher {
 		}
 		}
 	}
 	}
 
 
+	/**
+	 * Fires event for given marker change.
+	 *
+	 * @fires addMarker
+	 * @fires removeMarker
+	 * @param {String} type Change type.
+	 * @param {String} name Marker name.
+	 * @param {module:engine/model/range~Range} range Marker range.
+	 */
 	convertMarker( type, name, range ) {
 	convertMarker( type, name, range ) {
-		const consumable = this._createRangeConsumable( range );
+		const consumable = this._createMarkerConsumable( type, range );
 		const data = { name, range };
 		const data = { name, range };
 
 
 		this.fire( type + ':' + name, data, consumable, this.conversionApi );
 		this.fire( type + ':' + name, data, consumable, this.conversionApi );
@@ -360,14 +369,15 @@ export default class ModelConversionDispatcher {
 	}
 	}
 
 
 	/**
 	/**
-	 * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values of given `type` for each item from given `range`.
+	 * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values of given `type`
+	 * for each item from given `range`.
 	 *
 	 *
 	 * @private
 	 * @private
 	 * @param {module:engine/model/range~Range} range Affected range.
 	 * @param {module:engine/model/range~Range} range Affected range.
 	 * @param {String} type Consumable type.
 	 * @param {String} type Consumable type.
 	 * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
 	 * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
 	 */
 	 */
-	_createConsumableForType( range, type ) {
+	_createConsumableForRange( range, type ) {
 		const consumable = new Consumable();
 		const consumable = new Consumable();
 
 
 		for ( let item of range.getItems() ) {
 		for ( let item of range.getItems() ) {
@@ -396,10 +406,18 @@ export default class ModelConversionDispatcher {
 		return consumable;
 		return consumable;
 	}
 	}
 
 
-	_createRangeConsumable( range ) {
+	/**
+	 * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} for adding or removing marker on given `range`.
+	 *
+	 * @private
+	 * @param {'addMarker'|'removeMarker'} type Change type.
+	 * @param {module:engine/model/range~Range} range Range on which marker was added or removed.
+	 * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
+	 */
+	_createMarkerConsumable( type, range ) {
 		const consumable = new Consumable();
 		const consumable = new Consumable();
 
 
-		consumable.add( range, 'range' );
+		consumable.add( range, type );
 
 
 		return consumable;
 		return consumable;
 	}
 	}
@@ -570,6 +588,38 @@ export default class ModelConversionDispatcher {
 	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
 	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
 	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
 	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
 	 */
 	 */
+
+	/**
+	 * Fired when a new marker is added to the model.
+	 *
+	 * `addMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
+	 * `addMarker:<markerName>`. By specifying certain marker names, you can make the events even more gradual. For example,
+	 * markers can be named `foo:abc`, `foo:bar`, then it is possible to listen to `addMarker:foo` or `addMarker:foo:abc` and
+	 * `addMarker:foo:bar` events.
+	 *
+	 * @event addMarker
+	 * @param {Object} data Additional information about the change.
+	 * @param {String} data.name Marker name.
+	 * @param {module:engine/model/range~Range} data.range Marker range.
+	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
+	 */
+
+	/**
+	 * Fired when marker is removed from the model.
+	 *
+	 * `removeMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
+	 * `removeMarker:<markerName>`. By specifying certain marker names, you can make the events even more gradual. For example,
+	 * markers can be named `foo:abc`, `foo:bar`, then it is possible to listen to `removeMarker:foo` or `removeMarker:foo:abc` and
+	 * `removeMarker:foo:bar` events.
+	 *
+	 * @event removeMarker
+	 * @param {Object} data Additional information about the change.
+	 * @param {String} data.name Marker name.
+	 * @param {module:engine/model/range~Range} data.range Marker range.
+	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
+	 */
 }
 }
 
 
 mix( ModelConversionDispatcher, EmitterMixin );
 mix( ModelConversionDispatcher, EmitterMixin );

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

@@ -36,7 +36,7 @@ import {
 	convertCollapsedSelection,
 	convertCollapsedSelection,
 	convertSelectionAttribute
 	convertSelectionAttribute
 } from '../conversion/model-selection-to-view-converters.js';
 } from '../conversion/model-selection-to-view-converters.js';
-import { insertText, insertElement, wrap } from '../conversion/model-to-view-converters.js';
+import { insertText, insertElement, wrapItem } from '../conversion/model-to-view-converters.js';
 import isPlainObject from '../../utils/lib/lodash/isPlainObject.js';
 import isPlainObject from '../../utils/lib/lodash/isPlainObject.js';
 
 
 /**
 /**
@@ -214,7 +214,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	mapper.bindElements( node.root, viewDocumentFragment );
 	mapper.bindElements( node.root, viewDocumentFragment );
 
 
 	modelToView.on( 'insert:$text', insertText() );
 	modelToView.on( 'insert:$text', insertText() );
-	modelToView.on( 'addAttribute', wrap( ( value, data ) => {
+	modelToView.on( 'addAttribute', wrapItem( ( value, data ) => {
 		if ( data.item instanceof ModelTextProxy ) {
 		if ( data.item instanceof ModelTextProxy ) {
 			return new ViewAttributeElement( 'model-text-with-attributes', { [ data.attributeKey ]: stringifyAttributeValue( value ) } );
 			return new ViewAttributeElement( 'model-text-with-attributes', { [ data.attributeKey ]: stringifyAttributeValue( value ) } );
 		}
 		}

+ 90 - 40
packages/ckeditor5-engine/src/model/markerscollection.js

@@ -12,15 +12,15 @@ import mix from '../../utils/mix.js';
 /**
 /**
  * Manages and stores markers.
  * Manages and stores markers.
  *
  *
- * Markers are simply {@link module:engine/model/liverange~LiveRange live ranges} added to `MarkersCollection`.
+ * Markers are simply {@link module:engine/model/liverange~LiveRange live ranges} that were added to `MarkersCollection`.
  * Markers are used to represent information connected with model document. In contrary to
  * Markers are used to represent information connected with model document. In contrary to
  * {@link module:engine/model/node~Node nodes}, which are bits of data, markers are marking a part of model document.
  * {@link module:engine/model/node~Node nodes}, which are bits of data, markers are marking a part of model document.
- * Each live range/marker is added with `name` parameter. Name is used to group and identify markers. Multiple live
- * ranges can be added under the same name.
+ * Each live range is added with `name` parameter. Name is used to group and identify markers. Names have to be unique, but
+ * markers can be grouped by using common prefixes, separated with `:`, for example: `user:john` or `search:3`.
  *
  *
- * Whenever live range is added or removed from markers collection, markers collection fires `addMarker` and `removeMarker`
- * events. Same happens when a live range is changed due to changes in the model (both events are fired, `removeMarker` first,
- * then `addMarker`).
+ * Whenever live range is added or removed from `MarkersCollection`,
+ * {@link module:engine/model/markerscollection~MarkersCollection#event:addMarker addMarker event} and
+ * {@link module:engine/model/markerscollection~MarkersCollection#event:addMarker removeMarker event} are fired.
  *
  *
  * Markers can be converted to view by adding appropriate converters for
  * Markers can be converted to view by adding appropriate converters for
  * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addMarker addMarker} and
  * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addMarker addMarker} and
@@ -41,36 +41,42 @@ export default class MarkersCollection {
 	 */
 	 */
 	constructor() {
 	constructor() {
 		/**
 		/**
-		 * Stores range to marker name bindings for added ranges.
+		 * Stores marker name to range bindings for added ranges.
 		 *
 		 *
 		 * @private
 		 * @private
-		 * @member {Map} #_markerNames
+		 * @member {Map} #_nameToRange
 		 */
 		 */
-		this._markerNames = new Map();
+		this._nameToRange = new Map();
 	}
 	}
 
 
 	/**
 	/**
-	 * Sets a name for given live range and adds it to the markers collection. Multiple live ranges may have same name.
-	 * Markers can also be grouped while still having different names, i.e.: `search:22`, `search:37`.
+	 * Returns an iterator that iterates over all markers added to the collection. Each item returned by the iterator is an array
+	 * containing two elements, first is a marker {String name} and second is a marker {@link module:engine/model/range~Range range}.
 	 *
 	 *
-	 * Throws, if given `liveRange` is not an instance of {@link module:engine/model/liverange~LiveRange}.
+	 * @returns {Iterator}
+	 */
+	[ Symbol.iterator ]() {
+		return this._nameToRange.entries();
+	}
+
+	/**
+	 * Sets a name for given live range and adds it to the markers collection.
 	 *
 	 *
-	 * Throws, if given {@link module:engine/model/liverange~LiveRange LiveRange} instance is already added to the collection.
+	 * Throws, if given `markerName` was already used.
 	 *
 	 *
-	 * `MarkersCollection` instance listens to `change` event of all live ranges added to it and fires `removeMarker`
-	 * and `addMarker` events when the live range changes.
+	 * Throws, if given `liveRange` is not an instance of {@link module:engine/model/liverange~LiveRange}.
 	 *
 	 *
-	 * @param {module:engine/model/liverange~LiveRange} liveRange Live range to be added as a marker to markers collection.
 	 * @param {String} markerName Name to be associated with given `liveRange`.
 	 * @param {String} markerName Name to be associated with given `liveRange`.
+	 * @param {module:engine/model/liverange~LiveRange} liveRange Live range to be added as a marker to markers collection.
 	 */
 	 */
-	addRange( liveRange, markerName ) {
-		if ( this._markerNames.has( liveRange ) ) {
+	add( markerName, liveRange ) {
+		if ( this._nameToRange.has( markerName ) ) {
 			/**
 			/**
-			 * Given range instance was already added.
+			 * Marker with given name is already added.
 			 *
 			 *
-			 * @error markers-collection-add-range-range-already-added
+			 * @error markers-collection-add-name-exists
 			 */
 			 */
-			throw new CKEditorError( 'markers-collection-add-range-already-added: Given range instance was already added.' );
+			throw new CKEditorError( 'markers-collection-add-name-exists: Marker with given name is already added.' );
 		}
 		}
 
 
 		if ( !( liveRange instanceof LiveRange ) ) {
 		if ( !( liveRange instanceof LiveRange ) ) {
@@ -82,23 +88,43 @@ export default class MarkersCollection {
 			throw new CKEditorError( 'markers-collection-add-range-not-live-range: Added range is not an instance of LiveRange.' );
 			throw new CKEditorError( 'markers-collection-add-range-not-live-range: Added range is not an instance of LiveRange.' );
 		}
 		}
 
 
-		this._markerNames.set( liveRange, markerName );
-		this.fire( 'addMarker', markerName, Range.createFromRange( liveRange ) );
+		this._nameToRange.set( markerName, liveRange );
+		this.fire( 'add', markerName, Range.createFromRange( liveRange ) );
 	}
 	}
 
 
 	/**
 	/**
-	 * Removes a live range from markers collection and makes markers collection stop listening to that live range
-	 * `change` event.
+	 * Returns the live range that was added to `MarkersCollection` under given `markerName`.
 	 *
 	 *
-	 * @param {module:engine/model/liverange~LiveRange} liveRange Live range to be removed from markers collection.
-	 * @returns {Boolean} `true` is passed `liveRange` was found and removed from the markers collection, `false` otherwise.
+	 * @param {String} markerName Name of range to get.
+	 * @returns {module:engine/model/liverange~LiveRange|null} Range added to collection under given name or `null` if
+	 * no range was added with that name.
 	 */
 	 */
-	removeRange( liveRange ) {
-		const markerName = this._markerNames.get( liveRange );
+	get( markerName ) {
+		return this._nameToRange.get( markerName ) || null;
+	}
 
 
-		if ( markerName ) {
-			this._markerNames.delete( liveRange );
-			this.fire( 'removeMarker', markerName, Range.createFromRange( liveRange ) );
+	/**
+	 * Removes a live range from markers collection.
+	 *
+	 * @param {String|module:engine/model/liverange~LiveRange} nameOrRange Name of live range to remove or live range instance.
+	 * @returns {Boolean} `true` is passed if range was found and removed from the markers collection, `false` otherwise.
+	 */
+	remove( nameOrRange ) {
+		if ( nameOrRange instanceof LiveRange ) {
+			const name = this._getMarkerName( nameOrRange );
+
+			if ( name ) {
+				return this.remove( name );
+			}
+
+			return false;
+		}
+
+		const range = this._nameToRange.get( nameOrRange );
+
+		if ( range ) {
+			this._nameToRange.delete( nameOrRange );
+			this.fire( 'remove', nameOrRange, Range.createFromRange( range ) );
 
 
 			return true;
 			return true;
 		}
 		}
@@ -114,21 +140,28 @@ export default class MarkersCollection {
 	 *
 	 *
 	 * **Note**: this method does not change properties of `oldLiveRange`.
 	 * **Note**: this method does not change properties of `oldLiveRange`.
 	 *
 	 *
-	 * @param {module:engine/model/liverange~LiveRange} oldLiveRange Live range to be changed.
+	 * @param {String|module:engine/model/liverange~LiveRange} nameOrRange Name of range or range instance to be changed.
 	 * @param {module:engine/model/liverange~LiveRange} newLiveRange Live range to be added.
 	 * @param {module:engine/model/liverange~LiveRange} newLiveRange Live range to be added.
 	 * @returns {Boolean} `true` if `oldLiveRange` was found and changed, `false` otherwise.
 	 * @returns {Boolean} `true` if `oldLiveRange` was found and changed, `false` otherwise.
 	 */
 	 */
-	updateRange( oldLiveRange, newLiveRange ) {
-		const markerName = this._markerNames.get( oldLiveRange );
+	update( nameOrRange, newLiveRange ) {
+		if ( nameOrRange instanceof LiveRange ) {
+			const name = this._getMarkerName( nameOrRange );
 
 
-		if ( markerName ) {
-			this.removeRange( oldLiveRange );
-			this.addRange( newLiveRange, markerName );
+			if ( name ) {
+				return this.update( name, newLiveRange );
+			}
 
 
-			return true;
+			return false;
 		}
 		}
 
 
-		return false;
+		const removed = this.remove( nameOrRange );
+
+		if ( removed ) {
+			this.add( nameOrRange, newLiveRange );
+		}
+
+		return removed;
 	}
 	}
 
 
 	/**
 	/**
@@ -137,6 +170,23 @@ export default class MarkersCollection {
 	destroy() {
 	destroy() {
 		this.stopListening();
 		this.stopListening();
 	}
 	}
+
+	/**
+	 * Returns range for given name.
+	 *
+	 * @private
+	 * @param {module:engine/model/liverange~LiveRange} range Range for which name should be obtained.
+	 * @returns {String|null} Name of the range or `null` if range has not been added to the collection.
+	 */
+	_getMarkerName( range ) {
+		for ( let [ markerName, markerRange ] of this ) {
+			if ( markerRange == range ) {
+				return markerName;
+			}
+		}
+
+		return null;
+	}
 }
 }
 
 
 mix( MarkersCollection, EmitterMixin );
 mix( MarkersCollection, EmitterMixin );

+ 39 - 7
packages/ckeditor5-engine/src/view/writer.js

@@ -13,6 +13,7 @@ import AttributeElement from './attributeelement.js';
 import EmptyElement from './emptyelement.js';
 import EmptyElement from './emptyelement.js';
 import Text from './text.js';
 import Text from './text.js';
 import Range from './range.js';
 import Range from './range.js';
+import TreeWalker from './treewalker.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 import DocumentFragment from './documentfragment.js';
 import DocumentFragment from './documentfragment.js';
 import isIterable from '../../utils/isiterable.js';
 import isIterable from '../../utils/isiterable.js';
@@ -34,7 +35,8 @@ const writer = {
 	wrap,
 	wrap,
 	wrapPosition,
 	wrapPosition,
 	unwrap,
 	unwrap,
-	rename
+	rename,
+	breakViewRangePerContainer
 };
 };
 
 
 export default writer;
 export default writer;
@@ -270,6 +272,36 @@ export function mergeContainers( position ) {
 	return newPosition;
 	return newPosition;
 }
 }
 
 
+/**
+ * Breaks given `range` on a set of {@link module:engine/view/range~Range ranges}, that each are contained within a
+ * {@link module:engine/view/containerelement~ContainerElement container element}. After `range` is broken, it's "pieces" can
+ * be used by {@link module:engine/view/writer~writer} (which expects that passed ranges are contained within on container element).
+ *
+ * @function module:engine/view/writer~writer.breakViewRangePerContainer
+ * @param {module:engine/view/range~Range} range Range to break.
+ * @returns {Array.<module:engine/view/range~Range>} Ranges that combine into passed `viewRange`.
+ */
+export function breakViewRangePerContainer( range ) {
+	const ranges = [];
+	const walker = new TreeWalker( { boundaries: range } );
+
+	let start = range.start;
+
+	for ( let value of walker ) {
+		if ( value.item instanceof ContainerElement ) {
+			if ( !start.isEqual( value.previousPosition ) ) {
+				ranges.push( new Range( start, value.previousPosition ) );
+			}
+
+			start = value.nextPosition;
+		}
+	}
+
+	ranges.push( new Range( start, range.end ) );
+
+	return ranges;
+}
+
 /**
 /**
  * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
  * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
  * and merging them afterwards.
  * and merging them afterwards.
@@ -1110,19 +1142,19 @@ function isContainerOrFragment( node ) {
 	return node instanceof ContainerElement || node instanceof DocumentFragment;
 	return node instanceof ContainerElement || node instanceof DocumentFragment;
 }
 }
 
 
-// Checks if {@link module:engine/view/range~Range#start range start} and {@link module:engine/view/range~Range#end range end} are placed
-// inside same {@link module:engine/view/containerelement~ContainerElement container}.
-// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
+// Checks if {@link engine.view.Range#start range start} and {@link engine.view.Range#end range end} are placed
+// inside same {@link engine.view.ContainerElement container}.
+// Throws {@link utils.CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
 //
 //
-// @param {module:engine/view/range~Range} range
+// @param {engine.view.Range} range
 function validateRangeContainer( range ) {
 function validateRangeContainer( range ) {
 	const startContainer = getParentContainer( range.start );
 	const startContainer = getParentContainer( range.start );
 	const endContainer = getParentContainer( range.end );
 	const endContainer = getParentContainer( range.end );
 
 
 	if ( !startContainer || !endContainer || startContainer !== endContainer ) {
 	if ( !startContainer || !endContainer || startContainer !== endContainer ) {
 		/**
 		/**
-		 * Range container is invalid. This can happen if {@link module:engine/view/range~Range#start range start} and
-		 * {@link module:engine/view/range~Range#end range end} positions are not placed inside same container or
+		 * Range container is invalid. This can happen if {@link engine.view.Range#start range start} and
+		 * {@link engine.view.Range#end range end} positions are not placed inside same container or
 		 * parent container for these positions cannot be found.
 		 * parent container for these positions cannot be found.
 		 *
 		 *
 		 * @error view-writer-invalid-range-container
 		 * @error view-writer-invalid-range-container

+ 121 - 19
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -17,6 +17,7 @@ import buildModelConverter from 'ckeditor5/engine/conversion/buildmodelconverter
 
 
 import ModelDocument from 'ckeditor5/engine/model/document.js';
 import ModelDocument from 'ckeditor5/engine/model/document.js';
 import ModelPosition from 'ckeditor5/engine/model/position.js';
 import ModelPosition from 'ckeditor5/engine/model/position.js';
+import ModelElement from 'ckeditor5/engine/model/element.js';
 import ModelRange from 'ckeditor5/engine/model/range.js';
 import ModelRange from 'ckeditor5/engine/model/range.js';
 import ModelLiveRange from 'ckeditor5/engine/model/liverange.js';
 import ModelLiveRange from 'ckeditor5/engine/model/liverange.js';
 import ModelDocumentFragment from 'ckeditor5/engine/model/documentfragment.js';
 import ModelDocumentFragment from 'ckeditor5/engine/model/documentfragment.js';
@@ -109,7 +110,7 @@ describe( 'EditingController', () => {
 	describe( 'conversion', () => {
 	describe( 'conversion', () => {
 		let model, modelRoot, viewRoot, domRoot, editing, listener;
 		let model, modelRoot, viewRoot, domRoot, editing, listener;
 
 
-		before( () => {
+		beforeEach( () => {
 			listener = Object.create( EmitterMixin );
 			listener = Object.create( EmitterMixin );
 
 
 			model = new ModelDocument();
 			model = new ModelDocument();
@@ -125,15 +126,7 @@ describe( 'EditingController', () => {
 			model.schema.registerItem( 'div', '$block' );
 			model.schema.registerItem( 'div', '$block' );
 			buildModelConverter().for( editing.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 			buildModelConverter().for( editing.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 			buildModelConverter().for( editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
 			buildModelConverter().for( editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
-		} );
-
-		after( () => {
-			document.body.removeChild( domRoot );
-			listener.stopListening();
-			editing.destroy();
-		} );
 
 
-		beforeEach( () => {
 			// Note: The below code is highly overcomplicated due to #455.
 			// Note: The below code is highly overcomplicated due to #455.
 			model.selection.removeAllRanges();
 			model.selection.removeAllRanges();
 			modelRoot.removeChildren( 0, modelRoot.childCount );
 			modelRoot.removeChildren( 0, modelRoot.childCount );
@@ -154,6 +147,12 @@ describe( 'EditingController', () => {
 			} );
 			} );
 		} );
 		} );
 
 
+		afterEach( () => {
+			document.body.removeChild( domRoot );
+			listener.stopListening();
+			editing.destroy();
+		} );
+
 		it( 'should convert insertion', () => {
 		it( 'should convert insertion', () => {
 			expect( getViewData( editing.view ) ).to.equal( '<p>f{}oo</p><p></p><p>bar</p>' );
 			expect( getViewData( editing.view ) ).to.equal( '<p>f{}oo</p><p></p><p>bar</p>' );
 		} );
 		} );
@@ -259,11 +258,11 @@ describe( 'EditingController', () => {
 
 
 			sinon.spy( editing.modelToView, 'convertMarker' );
 			sinon.spy( editing.modelToView, 'convertMarker' );
 
 
-			model.markers.fire( 'addMarker', range, 'name' );
+			model.markers.fire( 'add', range, 'name' );
 
 
 			expect( editing.modelToView.convertMarker.calledWithExactly( 'addMarker', range, 'name' ) ).to.be.true;
 			expect( editing.modelToView.convertMarker.calledWithExactly( 'addMarker', range, 'name' ) ).to.be.true;
 
 
-			model.markers.fire( 'removeMarker', range, 'name' );
+			model.markers.fire( 'remove', range, 'name' );
 
 
 			expect( editing.modelToView.convertMarker.calledWithExactly( 'removeMarker', range, 'name' ) ).to.be.true;
 			expect( editing.modelToView.convertMarker.calledWithExactly( 'removeMarker', range, 'name' ) ).to.be.true;
 
 
@@ -273,28 +272,53 @@ describe( 'EditingController', () => {
 		it( 'should forward add marker event if content is inserted into a marker range', () => {
 		it( 'should forward add marker event if content is inserted into a marker range', () => {
 			const markerRange = ModelLiveRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 );
 			const markerRange = ModelLiveRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 );
 			const innerRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 );
 			const innerRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 );
+			const consumableMock = {
+				consume: () => true,
+				test: () => true
+			};
 
 
-			model.markers.addRange( markerRange, 'name' );
+			model.markers.add( 'name', markerRange );
 
 
 			sinon.spy( editing.modelToView, 'convertMarker' );
 			sinon.spy( editing.modelToView, 'convertMarker' );
 
 
 			editing.modelToView.fire( 'insert', {
 			editing.modelToView.fire( 'insert', {
 				range: innerRange
 				range: innerRange
-			} );
+			}, consumableMock, { dispatcher: editing.modelToView } );
 
 
 			expect( editing.modelToView.convertMarker.calledWithExactly( 'addMarker', 'name', innerRange ) ).to.be.true;
 			expect( editing.modelToView.convertMarker.calledWithExactly( 'addMarker', 'name', innerRange ) ).to.be.true;
 
 
 			editing.modelToView.convertMarker.restore();
 			editing.modelToView.convertMarker.restore();
 		} );
 		} );
 
 
-		it( 'should forward remove marker event if part of marker range is moved', () => {
+		it( 'should not start marker conversion if content is not inserted into any marker range', () => {
+			const markerRange = ModelLiveRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 );
+			const insertRange = ModelRange.createFromParentsAndOffsets( modelRoot, 6, modelRoot, 8 );
+			const consumableMock = {
+				consume: () => true,
+				test: () => true
+			};
+
+			model.markers.add( 'name', markerRange );
+
+			sinon.spy( editing.modelToView, 'convertMarker' );
+
+			editing.modelToView.fire( 'insert', {
+				range: insertRange
+			}, consumableMock, { dispatcher: editing.modelToView } );
+
+			expect( editing.modelToView.convertMarker.called ).to.be.false;
+
+			editing.modelToView.convertMarker.restore();
+		} );
+
+		it( 'should forward remove marker event if part of marker range is moved - intersecting', () => {
 			const markerRange = ModelLiveRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 );
 			const markerRange = ModelLiveRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 );
 			const consumableMock = {
 			const consumableMock = {
 				consume: () => true,
 				consume: () => true,
 				test: () => true
 				test: () => true
 			};
 			};
 
 
-			model.markers.addRange( markerRange, 'name' );
+			model.markers.add( 'name', markerRange );
 
 
 			sinon.spy( editing.modelToView, 'convertMarker' );
 			sinon.spy( editing.modelToView, 'convertMarker' );
 
 
@@ -302,7 +326,33 @@ describe( 'EditingController', () => {
 				sourcePosition: ModelPosition.createAt( modelRoot, 1 ),
 				sourcePosition: ModelPosition.createAt( modelRoot, 1 ),
 				targetPosition: ModelPosition.createAt( modelRoot, 2 ),
 				targetPosition: ModelPosition.createAt( modelRoot, 2 ),
 				item: modelRoot.getChild( 2 )
 				item: modelRoot.getChild( 2 )
-			}, consumableMock, { mapper: editing.mapper } );
+			}, consumableMock, { dispatcher: editing.modelToView, mapper: editing.mapper } );
+
+			expect( editing.modelToView.convertMarker.calledWith( 'removeMarker', 'name' ) ).to.be.true;
+
+			editing.modelToView.convertMarker.restore();
+		} );
+
+		it( 'should forward remove marker event if part of marker range is moved - inside', () => {
+			model.enqueueChanges( () => {
+				model.batch().insert( ModelPosition.createAt( model.getRoot(), 'end' ), new ModelElement( 'paragraph' ) );
+			} );
+
+			const markerRange = ModelLiveRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 2 );
+			const consumableMock = {
+				consume: () => true,
+				test: () => true
+			};
+
+			model.markers.add( 'name', markerRange );
+
+			sinon.spy( editing.modelToView, 'convertMarker' );
+
+			editing.modelToView.fire( 'move', {
+				sourcePosition: ModelPosition.createAt( modelRoot, 1 ),
+				targetPosition: ModelPosition.createAt( modelRoot, 3 ),
+				item: modelRoot.getChild( 3 )
+			}, consumableMock, { dispatcher: editing.modelToView, mapper: editing.mapper } );
 
 
 			expect( editing.modelToView.convertMarker.calledWith( 'removeMarker', 'name' ) ).to.be.true;
 			expect( editing.modelToView.convertMarker.calledWith( 'removeMarker', 'name' ) ).to.be.true;
 
 
@@ -310,26 +360,78 @@ describe( 'EditingController', () => {
 		} );
 		} );
 
 
 		it( 'should forward add marker event if content is moved into a marker range', () => {
 		it( 'should forward add marker event if content is moved into a marker range', () => {
+			model.enqueueChanges( () => {
+				model.batch().insert( ModelPosition.createAt( model.getRoot(), 'end' ), new ModelElement( 'paragraph' ) );
+			} );
+
 			const markerRange = ModelLiveRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 );
 			const markerRange = ModelLiveRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 );
 			const consumableMock = {
 			const consumableMock = {
 				consume: () => true,
 				consume: () => true,
 				test: () => true
 				test: () => true
 			};
 			};
 
 
-			model.markers.addRange( markerRange, 'name' );
+			model.markers.add( 'name', markerRange );
 
 
 			sinon.spy( editing.modelToView, 'convertMarker' );
 			sinon.spy( editing.modelToView, 'convertMarker' );
 
 
 			editing.modelToView.fire( 'move', {
 			editing.modelToView.fire( 'move', {
-				sourcePosition: ModelPosition.createAt( modelRoot, 2 ),
+				sourcePosition: ModelPosition.createAt( modelRoot, 3 ),
 				targetPosition: ModelPosition.createAt( modelRoot, 1 ),
 				targetPosition: ModelPosition.createAt( modelRoot, 1 ),
 				item: modelRoot.getChild( 1 )
 				item: modelRoot.getChild( 1 )
-			}, consumableMock, { mapper: editing.mapper } );
+			}, consumableMock, { dispatcher: editing.modelToView, mapper: editing.mapper } );
 
 
 			expect( editing.modelToView.convertMarker.calledWith( 'addMarker', 'name' ) ).to.be.true;
 			expect( editing.modelToView.convertMarker.calledWith( 'addMarker', 'name' ) ).to.be.true;
 
 
 			editing.modelToView.convertMarker.restore();
 			editing.modelToView.convertMarker.restore();
 		} );
 		} );
+
+		it( 'should not start marker conversion if moved content does not affect the marker', () => {
+			const markerRange = ModelLiveRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 );
+			const consumableMock = {
+				consume: () => true,
+				test: () => true
+			};
+
+			model.markers.add( 'name', markerRange );
+
+			sinon.spy( editing.modelToView, 'convertMarker' );
+
+			editing.modelToView.fire( 'move', {
+				sourcePosition: ModelPosition.createAt( modelRoot, 2 ),
+				targetPosition: ModelPosition.createAt( modelRoot, 0 ),
+				item: modelRoot.getChild( 2 )
+			}, consumableMock, { dispatcher: editing.modelToView, mapper: editing.mapper } );
+
+			expect( editing.modelToView.convertMarker.called ).to.be.false;
+
+			editing.modelToView.convertMarker.restore();
+		} );
+
+		it( 'should not start marker conversion if content is moved inside the marker', () => {
+			model.enqueueChanges( () => {
+				model.batch().insert( ModelPosition.createAt( model.getRoot(), 'end' ), new ModelElement( 'paragraph' ) );
+			} );
+
+			const markerRange = ModelLiveRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 4 );
+			const consumableMock = {
+				consume: () => true,
+				test: () => true
+			};
+
+			model.markers.add( 'name', markerRange );
+
+			sinon.spy( editing.modelToView, 'convertMarker' );
+
+			editing.modelToView.fire( 'move', {
+				sourcePosition: ModelPosition.createAt( modelRoot, 3 ),
+				targetPosition: ModelPosition.createAt( modelRoot, 1 ),
+				item: modelRoot.getChild( 1 )
+			}, consumableMock, { dispatcher: editing.modelToView, mapper: editing.mapper } );
+
+			expect( editing.modelToView.convertMarker.called ).to.be.false;
+
+			editing.modelToView.convertMarker.restore();
+		} );
 	} );
 	} );
 
 
 	describe( 'destroy', () => {
 	describe( 'destroy', () => {

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

@@ -32,8 +32,8 @@ import {
 	insertText,
 	insertText,
 	setAttribute,
 	setAttribute,
 	removeAttribute,
 	removeAttribute,
-	wrap,
-	unwrap,
+	wrapItem,
+	unwrapItem,
 	move,
 	move,
 	remove,
 	remove,
 	eventNameToConsumableType
 	eventNameToConsumableType
@@ -334,8 +334,8 @@ describe( 'image with caption converters', () => {
 describe( 'custom attribute handling for given element', () => {
 describe( 'custom attribute handling for given element', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		// NORMAL LINK MODEL TO VIEW CONVERTERS
 		// NORMAL LINK MODEL TO VIEW CONVERTERS
-		modelDispatcher.on( 'addAttribute:linkHref', wrap( ( value ) => new ViewAttributeElement( 'a', { href: value } ) ) );
-		modelDispatcher.on( 'addAttribute:linkTitle', wrap( ( value ) => new ViewAttributeElement( 'a', { title: value } ) ) );
+		modelDispatcher.on( 'addAttribute:linkHref', wrapItem( ( value ) => new ViewAttributeElement( 'a', { href: value } ) ) );
+		modelDispatcher.on( 'addAttribute:linkTitle', wrapItem( ( value ) => new ViewAttributeElement( 'a', { title: value } ) ) );
 
 
 		const changeLinkAttribute = function( elementCreator ) {
 		const changeLinkAttribute = function( elementCreator ) {
 			return ( evt, data, consumable, conversionApi ) => {
 			return ( evt, data, consumable, conversionApi ) => {
@@ -364,12 +364,12 @@ describe( 'custom attribute handling for given element', () => {
 
 
 		modelDispatcher.on(
 		modelDispatcher.on(
 			'removeAttribute:linkHref',
 			'removeAttribute:linkHref',
-			unwrap( ( value ) => new ViewAttributeElement( 'a', { href: value } ) )
+			unwrapItem( ( value ) => new ViewAttributeElement( 'a', { href: value } ) )
 		);
 		);
 
 
 		modelDispatcher.on(
 		modelDispatcher.on(
 			'removeAttribute:linkTitle',
 			'removeAttribute:linkTitle',
-			unwrap( ( value ) => new ViewAttributeElement( 'a', { title: value } ) )
+			unwrapItem( ( value ) => new ViewAttributeElement( 'a', { title: value } ) )
 		);
 		);
 
 
 		// NORMAL LINK VIEW TO MODEL CONVERTERS
 		// NORMAL LINK VIEW TO MODEL CONVERTERS
@@ -538,9 +538,9 @@ describe( 'custom attribute handling for given element', () => {
 		let expected = '<div><blockquote>foo<a href="foo.html" title="Foo source">see source</a></blockquote></div>';
 		let expected = '<div><blockquote>foo<a href="foo.html" title="Foo source">see source</a></blockquote></div>';
 		expect( viewToString( viewRoot ) ).to.equal( expected );
 		expect( viewToString( viewRoot ) ).to.equal( expected );
 
 
-		modelDispatcher.on( 'addAttribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
-		modelDispatcher.on( 'changeAttribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
-		modelDispatcher.on( 'removeAttribute:bold', unwrap( new ViewAttributeElement( 'strong' ) ) );
+		modelDispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
+		modelDispatcher.on( 'changeAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
+		modelDispatcher.on( 'removeAttribute:bold', unwrapItem( new ViewAttributeElement( 'strong' ) ) );
 
 
 		modelElement.appendChildren( new ModelText( 'bar', { bold: true } ) );
 		modelElement.appendChildren( new ModelText( 'bar', { bold: true } ) );
 		modelDispatcher.convertInsertion( ModelRange.createFromParentsAndOffsets( modelElement, 3, modelElement, 6 ) );
 		modelDispatcher.convertInsertion( ModelRange.createFromParentsAndOffsets( modelElement, 3, modelElement, 6 ) );
@@ -675,9 +675,9 @@ describe( 'universal converter', () => {
 
 
 		// "Real" converters -- added with higher priority. Should overwrite the "universal" converters.
 		// "Real" converters -- added with higher priority. Should overwrite the "universal" converters.
 		modelDispatcher.on( 'insert:image', insertElement( new ViewContainerElement( 'img' ) ) );
 		modelDispatcher.on( 'insert:image', insertElement( new ViewContainerElement( 'img' ) ) );
-		modelDispatcher.on( 'addAttribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
-		modelDispatcher.on( 'changeAttribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
-		modelDispatcher.on( 'removeAttribute:bold', unwrap( new ViewAttributeElement( 'strong' ) ) );
+		modelDispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
+		modelDispatcher.on( 'changeAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
+		modelDispatcher.on( 'removeAttribute:bold', unwrapItem( new ViewAttributeElement( 'strong' ) ) );
 
 
 		viewDispatcher.on( 'element:img', ( evt, data, consumable ) => {
 		viewDispatcher.on( 'element:img', ( evt, data, consumable ) => {
 			if ( consumable.consume( data.input, { name: true } ) ) {
 			if ( consumable.consume( data.input, { name: true } ) ) {

+ 6 - 13
packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js

@@ -36,6 +36,8 @@ import {
 
 
 import { createRangeOnElementOnly } from 'tests/engine/model/_utils/utils.js';
 import { createRangeOnElementOnly } from 'tests/engine/model/_utils/utils.js';
 
 
+import CKEditorError from 'ckeditor5/utils/ckeditorerror.js';
+
 function viewAttributesToString( item ) {
 function viewAttributesToString( item ) {
 	let result = '';
 	let result = '';
 
 
@@ -422,18 +424,9 @@ describe( 'Model converter builder', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	it( 'should do nothing on model element to view attribute conversion', () => {
-		buildModelConverter().for( dispatcher ).fromElement( 'div' ).toElement( 'div' );
-		// Should do nothing:
-		buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toAttribute( 'paragraph', true );
-		// If above would do something this one would not be fired:
-		buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
-
-		let modelElement = new ModelElement( 'div', null, new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) ) );
-		modelRoot.appendChildren( modelElement );
-
-		dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
-
-		expect( viewToString( viewRoot ) ).to.equal( '<div><div><p>foobar</p></div></div>' );
+	it( 'should throw when trying to build model element to view attribute converter', () => {
+		expect( () => {
+			buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toAttribute( 'paragraph', true );
+		} ).to.throw( CKEditorError, /^build-model-converter-non-attribute-to-attribute/ );
 	} );
 	} );
 } );
 } );

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

@@ -28,7 +28,7 @@ import {
 import {
 import {
 	insertElement,
 	insertElement,
 	insertText,
 	insertText,
-	wrap
+	wrapItem
 } from 'ckeditor5/engine/conversion/model-to-view-converters.js';
 } from 'ckeditor5/engine/conversion/model-to-view-converters.js';
 
 
 import { stringify as stringifyView } from 'ckeditor5/engine/dev-utils/view.js';
 import { stringify as stringifyView } from 'ckeditor5/engine/dev-utils/view.js';
@@ -55,7 +55,7 @@ beforeEach( () => {
 	dispatcher = new ModelConversionDispatcher( { mapper, viewSelection } );
 	dispatcher = new ModelConversionDispatcher( { mapper, viewSelection } );
 
 
 	dispatcher.on( 'insert:$text', insertText() );
 	dispatcher.on( 'insert:$text', insertText() );
-	dispatcher.on( 'addAttribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
+	dispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
 
 
 	// Default selection converters.
 	// Default selection converters.
 	dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
 	dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
@@ -269,7 +269,7 @@ describe( 'clean-up', () => {
 	describe( 'clearAttributes', () => {
 	describe( 'clearAttributes', () => {
 		it( 'should remove all ranges before adding new range', () => {
 		it( 'should remove all ranges before adding new range', () => {
 			dispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'b' ) ) );
 			dispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'b' ) ) );
-			dispatcher.on( 'addAttribute:style', wrap( new ViewAttributeElement( 'b' ) ) );
+			dispatcher.on( 'addAttribute:style', wrapItem( new ViewAttributeElement( 'b' ) ) );
 
 
 			test(
 			test(
 				[ 3, 3 ],
 				[ 3, 3 ],
@@ -291,7 +291,7 @@ describe( 'clean-up', () => {
 
 
 		it( 'should do nothing if the attribute element had been already removed', () => {
 		it( 'should do nothing if the attribute element had been already removed', () => {
 			dispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'b' ) ) );
 			dispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'b' ) ) );
-			dispatcher.on( 'addAttribute:style', wrap( new ViewAttributeElement( 'b' ) ) );
+			dispatcher.on( 'addAttribute:style', wrapItem( new ViewAttributeElement( 'b' ) ) );
 
 
 			test(
 			test(
 				[ 3, 3 ],
 				[ 3, 3 ],
@@ -338,7 +338,7 @@ describe( 'using element creator for attributes conversion', () => {
 		}
 		}
 
 
 		dispatcher.on( 'selectionAttribute:theme', convertSelectionAttribute( themeElementCreator ) );
 		dispatcher.on( 'selectionAttribute:theme', convertSelectionAttribute( themeElementCreator ) );
-		dispatcher.on( 'addAttribute:theme', wrap( themeElementCreator ) );
+		dispatcher.on( 'addAttribute:theme', wrapItem( themeElementCreator ) );
 
 
 		dispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
 		dispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
 	} );
 	} );

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

@@ -24,10 +24,10 @@ import {
 	insertText,
 	insertText,
 	setAttribute,
 	setAttribute,
 	removeAttribute,
 	removeAttribute,
-	wrap,
-	unwrap,
-	wrapMarker,
-	unwrapMarker,
+	wrapItem,
+	unwrapItem,
+	wrapRange,
+	unwrapRange,
 	move,
 	move,
 	remove,
 	remove,
 	rename
 	rename
@@ -263,8 +263,8 @@ describe( 'wrap/unwrap', () => {
 		modelRoot.appendChildren( modelElement );
 		modelRoot.appendChildren( modelElement );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'insert:$text', insertText() );
-		dispatcher.on( 'addAttribute:bold', wrap( viewB ) );
-		dispatcher.on( 'removeAttribute:bold', unwrap( viewB ) );
+		dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
+		dispatcher.on( 'removeAttribute:bold', unwrapItem( viewB ) );
 
 
 		dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 		dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 
 
@@ -290,8 +290,8 @@ describe( 'wrap/unwrap', () => {
 		modelRoot.appendChildren( modelElement );
 		modelRoot.appendChildren( modelElement );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'insert:$text', insertText() );
-		dispatcher.on( 'addAttribute:style', wrap( elementGenerator ) );
-		dispatcher.on( 'removeAttribute:style', unwrap( elementGenerator ) );
+		dispatcher.on( 'addAttribute:style', wrapItem( elementGenerator ) );
+		dispatcher.on( 'removeAttribute:style', unwrapItem( elementGenerator ) );
 
 
 		dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 		dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 
 
@@ -318,8 +318,8 @@ describe( 'wrap/unwrap', () => {
 		modelRoot.appendChildren( modelElement );
 		modelRoot.appendChildren( modelElement );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'insert:$text', insertText() );
-		dispatcher.on( 'addAttribute:link', wrap( elementGenerator ) );
-		dispatcher.on( 'changeAttribute:link', wrap( elementGenerator ) );
+		dispatcher.on( 'addAttribute:link', wrapItem( elementGenerator ) );
+		dispatcher.on( 'changeAttribute:link', wrapItem( elementGenerator ) );
 
 
 		dispatcher.convertInsertion(
 		dispatcher.convertInsertion(
 			ModelRange.createIn( modelRoot )
 			ModelRange.createIn( modelRoot )
@@ -348,8 +348,8 @@ describe( 'wrap/unwrap', () => {
 		modelRoot.appendChildren( modelElement );
 		modelRoot.appendChildren( modelElement );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'insert:$text', insertText() );
-		dispatcher.on( 'addAttribute:bold', wrap( viewB ) );
-		dispatcher.on( 'removeAttribute:bold', unwrap( viewB ) );
+		dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
+		dispatcher.on( 'removeAttribute:bold', unwrapItem( viewB ) );
 
 
 		dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 		dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
 
 
@@ -370,7 +370,7 @@ describe( 'wrap/unwrap', () => {
 		modelRoot.appendChildren( modelElement );
 		modelRoot.appendChildren( modelElement );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'insert:$text', insertText() );
-		dispatcher.on( 'addAttribute:bold', wrap( viewB ) );
+		dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
 		dispatcher.on( 'addAttribute:bold', ( evt, data, consumable ) => {
 		dispatcher.on( 'addAttribute:bold', ( evt, data, consumable ) => {
 			consumable.consume( data.item, 'addAttribute:bold' );
 			consumable.consume( data.item, 'addAttribute:bold' );
 		}, { priority: 'high' } );
 		}, { priority: 'high' } );
@@ -388,8 +388,8 @@ describe( 'wrap/unwrap', () => {
 		modelRoot.appendChildren( modelElement );
 		modelRoot.appendChildren( modelElement );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'insert:$text', insertText() );
-		dispatcher.on( 'addAttribute:bold', wrap( viewB ) );
-		dispatcher.on( 'removeAttribute:bold', unwrap( viewB ) );
+		dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
+		dispatcher.on( 'removeAttribute:bold', unwrapItem( viewB ) );
 		dispatcher.on( 'removeAttribute:bold', ( evt, data, consumable ) => {
 		dispatcher.on( 'removeAttribute:bold', ( evt, data, consumable ) => {
 			consumable.consume( data.item, 'removeAttribute:bold' );
 			consumable.consume( data.item, 'removeAttribute:bold' );
 		}, { priority: 'high' } );
 		}, { priority: 'high' } );
@@ -407,7 +407,7 @@ describe( 'wrap/unwrap', () => {
 	} );
 	} );
 } );
 } );
 
 
-describe( 'wrapMarker/unwrapMarker', () => {
+describe( 'wrapRange/unwrapRange', () => {
 	let modelText, rangeJohn, rangeAlice, modelElement;
 	let modelText, rangeJohn, rangeAlice, modelElement;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
@@ -428,8 +428,8 @@ describe( 'wrapMarker/unwrapMarker', () => {
 	it( 'should convert adding/removing of marker into wrapping element in a view', () => {
 	it( 'should convert adding/removing of marker into wrapping element in a view', () => {
 		const viewSpan = new ViewAttributeElement( 'span', { class: 'name' } );
 		const viewSpan = new ViewAttributeElement( 'span', { class: 'name' } );
 
 
-		dispatcher.on( 'addMarker:name', wrapMarker( viewSpan ) );
-		dispatcher.on( 'removeMarker:name', unwrapMarker( viewSpan ) );
+		dispatcher.on( 'addMarker:name', wrapRange( viewSpan ) );
+		dispatcher.on( 'removeMarker:name', unwrapRange( viewSpan ) );
 
 
 		dispatcher.convertMarker( 'addMarker', 'name', rangeJohn );
 		dispatcher.convertMarker( 'addMarker', 'name', rangeJohn );
 
 
@@ -443,8 +443,8 @@ describe( 'wrapMarker/unwrapMarker', () => {
 	it( 'should support collapsed markers', () => {
 	it( 'should support collapsed markers', () => {
 		const viewSpan = new ViewAttributeElement( 'span' );
 		const viewSpan = new ViewAttributeElement( 'span' );
 
 
-		dispatcher.on( 'addMarker:name', wrapMarker( viewSpan ) );
-		dispatcher.on( 'removeMarker:name', unwrapMarker( viewSpan ) );
+		dispatcher.on( 'addMarker:name', wrapRange( viewSpan ) );
+		dispatcher.on( 'removeMarker:name', unwrapRange( viewSpan ) );
 
 
 		const rangeP = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 0 );
 		const rangeP = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 0 );
 
 
@@ -469,8 +469,8 @@ describe( 'wrapMarker/unwrapMarker', () => {
 			return new ViewAttributeElement( 'span', { class: name } );
 			return new ViewAttributeElement( 'span', { class: name } );
 		};
 		};
 
 
-		dispatcher.on( 'addMarker:name', wrapMarker( converterCallback ) );
-		dispatcher.on( 'removeMarker:name', unwrapMarker( converterCallback ) );
+		dispatcher.on( 'addMarker:name', wrapRange( converterCallback ) );
+		dispatcher.on( 'removeMarker:name', unwrapRange( converterCallback ) );
 
 
 		dispatcher.convertMarker( 'addMarker', 'name:john', rangeJohn );
 		dispatcher.convertMarker( 'addMarker', 'name:john', rangeJohn );
 
 
@@ -508,8 +508,8 @@ describe( 'wrapMarker/unwrapMarker', () => {
 		const range = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement3, 1 );
 		const range = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement3, 1 );
 		const viewSpan = new ViewAttributeElement( 'span', { class: 'name' } );
 		const viewSpan = new ViewAttributeElement( 'span', { class: 'name' } );
 
 
-		dispatcher.on( 'addMarker:name', wrapMarker( viewSpan ) );
-		dispatcher.on( 'removeMarker:name', unwrapMarker( viewSpan ) );
+		dispatcher.on( 'addMarker:name', wrapRange( viewSpan ) );
+		dispatcher.on( 'removeMarker:name', unwrapRange( viewSpan ) );
 
 
 		dispatcher.convertMarker( 'addMarker', 'name', range );
 		dispatcher.convertMarker( 'addMarker', 'name', range );
 
 
@@ -522,16 +522,16 @@ describe( 'wrapMarker/unwrapMarker', () => {
 		expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p><p>22</p><p>333</p></div>' );
 		expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p><p>22</p><p>333</p></div>' );
 	} );
 	} );
 
 
-	it( 'should be possible to override wrapMarker', () => {
+	it( 'should be possible to override wrapRange', () => {
 		const converterCallback = ( data ) => {
 		const converterCallback = ( data ) => {
 			const name = data.name.split( ':' )[ 1 ];
 			const name = data.name.split( ':' )[ 1 ];
 
 
 			return new ViewAttributeElement( 'span', { class: name } );
 			return new ViewAttributeElement( 'span', { class: name } );
 		};
 		};
 
 
-		dispatcher.on( 'addMarker:name', wrapMarker( converterCallback ) );
+		dispatcher.on( 'addMarker:name', wrapRange( converterCallback ) );
 		dispatcher.on( 'addMarker:name:alice', ( evt, data, consumable ) => {
 		dispatcher.on( 'addMarker:name:alice', ( evt, data, consumable ) => {
-			consumable.consume( data.range, 'range' );
+			consumable.consume( data.range, 'addMarker' );
 		}, { priority: 'high' } );
 		}, { priority: 'high' } );
 
 
 		dispatcher.convertMarker( 'addMarker', 'name:john', rangeJohn );
 		dispatcher.convertMarker( 'addMarker', 'name:john', rangeJohn );
@@ -540,17 +540,17 @@ describe( 'wrapMarker/unwrapMarker', () => {
 		expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span class="john">ob</span>ar</p></div>' );
 		expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span class="john">ob</span>ar</p></div>' );
 	} );
 	} );
 
 
-	it( 'should be possible to override unwrapMarker', () => {
+	it( 'should be possible to override unwrapRange', () => {
 		const converterCallback = ( data ) => {
 		const converterCallback = ( data ) => {
 			const name = data.name.split( ':' )[ 1 ];
 			const name = data.name.split( ':' )[ 1 ];
 
 
 			return new ViewAttributeElement( 'span', { class: name } );
 			return new ViewAttributeElement( 'span', { class: name } );
 		};
 		};
 
 
-		dispatcher.on( 'addMarker:name', wrapMarker( converterCallback ) );
-		dispatcher.on( 'removeMarker:name', unwrapMarker( converterCallback ) );
+		dispatcher.on( 'addMarker:name', wrapRange( converterCallback ) );
+		dispatcher.on( 'removeMarker:name', unwrapRange( converterCallback ) );
 		dispatcher.on( 'removeMarker:name:john', ( evt, data, consumable ) => {
 		dispatcher.on( 'removeMarker:name:john', ( evt, data, consumable ) => {
-			consumable.consume( data.range, 'range' );
+			consumable.consume( data.range, 'removeMarker' );
 		}, { priority: 'high' } );
 		}, { priority: 'high' } );
 
 
 		dispatcher.convertMarker( 'addMarker', 'name:john', rangeJohn );
 		dispatcher.convertMarker( 'addMarker', 'name:john', rangeJohn );
@@ -565,10 +565,10 @@ describe( 'wrapMarker/unwrapMarker', () => {
 	it( 'should not convert if view element is not returned by element generating function', () => {
 	it( 'should not convert if view element is not returned by element generating function', () => {
 		const converterCallback = () => null;
 		const converterCallback = () => null;
 
 
-		dispatcher.on( 'addMarker:name', wrapMarker( converterCallback ) );
+		dispatcher.on( 'addMarker:name', wrapRange( converterCallback ) );
 		dispatcher.on( 'addMarker:name', ( evt, data, consumable ) => {
 		dispatcher.on( 'addMarker:name', ( evt, data, consumable ) => {
 			// Check whether value was not consumed from `consumable`.
 			// Check whether value was not consumed from `consumable`.
-			expect( consumable.test( data.range, 'range' ) ).to.be.true;
+			expect( consumable.test( data.range, 'addMarker' ) ).to.be.true;
 		} );
 		} );
 
 
 		dispatcher.convertMarker( 'addMarker', 'name', rangeJohn );
 		dispatcher.convertMarker( 'addMarker', 'name', rangeJohn );
@@ -581,10 +581,10 @@ describe( 'wrapMarker/unwrapMarker', () => {
 			const converterCallbackName = ( data ) => new ViewAttributeElement( 'span', { class: data.name.split( ':' )[ 1 ] } );
 			const converterCallbackName = ( data ) => new ViewAttributeElement( 'span', { class: data.name.split( ':' )[ 1 ] } );
 			const converterCallbackSearch = () => new ViewAttributeElement( 'em', { class: 'search' } );
 			const converterCallbackSearch = () => new ViewAttributeElement( 'em', { class: 'search' } );
 
 
-			dispatcher.on( 'addMarker:name', wrapMarker( converterCallbackName ) );
-			dispatcher.on( 'removeMarker:name', unwrapMarker( converterCallbackName ) );
-			dispatcher.on( 'addMarker:search', wrapMarker( converterCallbackSearch ) );
-			dispatcher.on( 'removeMarker:search', unwrapMarker( converterCallbackSearch ) );
+			dispatcher.on( 'addMarker:name', wrapRange( converterCallbackName ) );
+			dispatcher.on( 'removeMarker:name', unwrapRange( converterCallbackName ) );
+			dispatcher.on( 'addMarker:search', wrapRange( converterCallbackSearch ) );
+			dispatcher.on( 'removeMarker:search', unwrapRange( converterCallbackSearch ) );
 
 
 			const range = rangeAlice;
 			const range = rangeAlice;
 
 
@@ -616,10 +616,10 @@ describe( 'wrapMarker/unwrapMarker', () => {
 			};
 			};
 			const converterCallbackSearch = () => new ViewAttributeElement( 'em', { class: 'search' } );
 			const converterCallbackSearch = () => new ViewAttributeElement( 'em', { class: 'search' } );
 
 
-			dispatcher.on( 'addMarker:name', wrapMarker( converterCallbackName ) );
-			dispatcher.on( 'removeMarker:name', unwrapMarker( converterCallbackName ) );
-			dispatcher.on( 'addMarker:search', wrapMarker( converterCallbackSearch ) );
-			dispatcher.on( 'removeMarker:search', unwrapMarker( converterCallbackSearch ) );
+			dispatcher.on( 'addMarker:name', wrapRange( converterCallbackName ) );
+			dispatcher.on( 'removeMarker:name', unwrapRange( converterCallbackName ) );
+			dispatcher.on( 'addMarker:search', wrapRange( converterCallbackSearch ) );
+			dispatcher.on( 'removeMarker:search', unwrapRange( converterCallbackSearch ) );
 
 
 			const range = rangeJohn;
 			const range = rangeJohn;
 
 
@@ -644,8 +644,8 @@ describe( 'wrapMarker/unwrapMarker', () => {
 		it( 'non-collapsed markers intersecting with same priority', () => {
 		it( 'non-collapsed markers intersecting with same priority', () => {
 			const converterCallbackSearch = () => new ViewAttributeElement( 'em', { class: 'search' } );
 			const converterCallbackSearch = () => new ViewAttributeElement( 'em', { class: 'search' } );
 
 
-			dispatcher.on( 'addMarker:search', wrapMarker( converterCallbackSearch ) );
-			dispatcher.on( 'removeMarker:search', unwrapMarker( converterCallbackSearch ) );
+			dispatcher.on( 'addMarker:search', wrapRange( converterCallbackSearch ) );
+			dispatcher.on( 'removeMarker:search', unwrapRange( converterCallbackSearch ) );
 
 
 			const range1 = ModelRange.createFromParentsAndOffsets( modelElement, 1, modelElement, 3 );
 			const range1 = ModelRange.createFromParentsAndOffsets( modelElement, 1, modelElement, 3 );
 			const range2 = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 );
 			const range2 = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 );
@@ -679,8 +679,8 @@ describe( 'wrapMarker/unwrapMarker', () => {
 				return element;
 				return element;
 			};
 			};
 
 
-			dispatcher.on( 'addMarker:name', wrapMarker( converterCallbackName ) );
-			dispatcher.on( 'removeMarker:name', unwrapMarker( converterCallbackName ) );
+			dispatcher.on( 'addMarker:name', wrapRange( converterCallbackName ) );
+			dispatcher.on( 'removeMarker:name', unwrapRange( converterCallbackName ) );
 
 
 			const range1 = ModelRange.createFromParentsAndOffsets( modelElement, 1, modelElement, 5 );
 			const range1 = ModelRange.createFromParentsAndOffsets( modelElement, 1, modelElement, 5 );
 			const range2 = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 2 );
 			const range2 = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 2 );

+ 103 - 50
packages/ckeditor5-engine/tests/model/markerscollection.js

@@ -11,94 +11,123 @@ import LiveRange from 'ckeditor5/engine/model/liverange.js';
 import Document from 'ckeditor5/engine/model/document.js';
 import Document from 'ckeditor5/engine/model/document.js';
 import CKEditorError from 'ckeditor5/utils/ckeditorerror.js';
 import CKEditorError from 'ckeditor5/utils/ckeditorerror.js';
 
 
-let markers, live, doc, root;
+describe( 'MarkersCollection', () => {
+	let markers, live, doc, root;
 
 
-beforeEach( () => {
-	doc = new Document();
-	markers = new MarkersCollection();
+	beforeEach( () => {
+		doc = new Document();
+		markers = new MarkersCollection();
 
 
-	root = doc.createRoot();
-	live = LiveRange.createFromParentsAndOffsets( root, 0, root, 4 );
-} );
+		root = doc.createRoot();
+		live = LiveRange.createFromParentsAndOffsets( root, 0, root, 4 );
+	} );
 
 
-afterEach( () => {
-	markers.destroy();
-	live.detach();
-} );
+	afterEach( () => {
+		markers.destroy();
+		live.detach();
+	} );
 
 
-describe( 'MarkersCollection', () => {
-	describe( 'addRange', () => {
+	describe( 'add', () => {
 		it( 'should throw if passed parameter is not a LiveRange', () => {
 		it( 'should throw if passed parameter is not a LiveRange', () => {
 			const range = Range.createFromParentsAndOffsets( root, 1, root, 3 );
 			const range = Range.createFromParentsAndOffsets( root, 1, root, 3 );
 
 
 			expect( () => {
 			expect( () => {
-				markers.addRange( range, 'name' );
+				markers.add( 'name', range );
 			} ).to.throw( CKEditorError, /^markers-collection-add-range-not-live-range/ );
 			} ).to.throw( CKEditorError, /^markers-collection-add-range-not-live-range/ );
 		} );
 		} );
 
 
-		it( 'should fire addMarker event when range is added', () => {
+		it( 'should fire add event when range is added', () => {
 			sinon.spy( markers, 'fire' );
 			sinon.spy( markers, 'fire' );
 
 
-			markers.on( 'addMarker', ( evt, name, range ) => {
+			markers.on( 'add', ( evt, name, range ) => {
 				expect( name ).to.equal( 'name' );
 				expect( name ).to.equal( 'name' );
 				expect( range.isEqual( live ) ).to.be.true;
 				expect( range.isEqual( live ) ).to.be.true;
 				expect( range ).not.to.equal( live );
 				expect( range ).not.to.equal( live );
 			} );
 			} );
 
 
-			markers.addRange( live, 'name' );
+			markers.add( 'name', live );
 
 
-			expect( markers.fire.calledWith( 'addMarker' ) ).to.be.true;
+			expect( markers.fire.calledWith( 'add' ) ).to.be.true;
 		} );
 		} );
 
 
-		it( 'should throw if given range was already added', () => {
-			markers.addRange( live, 'name' );
+		it( 'should throw if given name was already added', () => {
+			const other = LiveRange.createFromParentsAndOffsets( root, 0, root, 4 );
+			markers.add( 'name', other );
+
+			other.detach();
 
 
 			expect( () => {
 			expect( () => {
-				markers.addRange( live, 'otherName' );
-			} ).to.throw( CKEditorError, /^markers-collection-add-range-already-added/ );
+				markers.add( 'name', live );
+			} ).to.throw( CKEditorError, /^markers-collection-add-name-exists/ );
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'removeRange', () => {
-		it( 'should return false if range has not been found in collection', () => {
-			const result = markers.removeRange( live );
+	describe( 'get', () => {
+		it( 'should return range added to the collection with given name', () => {
+			markers.add( 'name', live );
 
 
-			expect( result ).to.be.false;
+			expect( markers.get( 'name' ) ).to.equal( live );
 		} );
 		} );
 
 
-		it( 'should return true and fire removeMarker event if range is removed', () => {
-			markers.addRange( live, 'name' );
+		it( 'should return null if range with given name has not been found', () => {
+			expect( markers.get( 'name' ) ).to.be.null;
+		} );
+	} );
+
+	describe( 'remove', () => {
+		it( 'should return true and fire remove event if range is removed - passed name', () => {
+			markers.add( 'name', live );
 
 
 			sinon.spy( markers, 'fire' );
 			sinon.spy( markers, 'fire' );
 
 
-			markers.on( 'removeMarker', ( evt, name, range ) => {
+			markers.on( 'remove', ( evt, name, range ) => {
 				expect( name ).to.equal( 'name' );
 				expect( name ).to.equal( 'name' );
 				expect( range.isEqual( live ) ).to.be.true;
 				expect( range.isEqual( live ) ).to.be.true;
 				expect( range ).not.to.equal( live );
 				expect( range ).not.to.equal( live );
 			} );
 			} );
 
 
-			const result = markers.removeRange( live );
+			const result = markers.remove( 'name' );
 
 
 			expect( result ).to.be.true;
 			expect( result ).to.be.true;
-			expect( markers.fire.calledWith( 'removeMarker' ) ).to.be.true;
+			expect( markers.fire.calledWith( 'remove' ) ).to.be.true;
 		} );
 		} );
 
 
-		it( 'should make MarkersCollection stop listening to range change event', () => {
-			markers.addRange( live, 'name' );
-			markers.removeRange( live );
+		it( 'should return true and fire remove event if range is removed - passed range', () => {
+			markers.add( 'name', live );
 
 
 			sinon.spy( markers, 'fire' );
 			sinon.spy( markers, 'fire' );
 
 
-			// Simulate LiveRange change.
-			const oldRange = Range.createFromRange( live );
-			live.end.path[ 0 ]++;
-			live.fire( 'change', oldRange );
+			markers.on( 'remove', ( evt, name, range ) => {
+				expect( name ).to.equal( 'name' );
+				expect( range.isEqual( live ) ).to.be.true;
+				expect( range ).not.to.equal( live );
+			} );
+
+			const result = markers.remove( live );
+
+			expect( result ).to.be.true;
+			expect( markers.fire.calledWith( 'remove' ) ).to.be.true;
+		} );
+
+		it( 'should return false if name has not been found in collection', () => {
+			const result = markers.remove( 'name' );
+
+			expect( result ).to.be.false;
+		} );
+
+		it( 'should return false if range has not been found in collection', () => {
+			markers.add( 'name', live );
+
+			const other = LiveRange.createFromParentsAndOffsets( root, 0, root, 4 );
+			const result = markers.remove( other );
 
 
-			expect( markers.fire.called ).to.be.false;
+			other.detach();
+
+			expect( result ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'updateRange', () => {
+	describe( 'update', () => {
 		let newLive;
 		let newLive;
 
 
 		beforeEach( () => {
 		beforeEach( () => {
@@ -109,24 +138,48 @@ describe( 'MarkersCollection', () => {
 			newLive.detach();
 			newLive.detach();
 		} );
 		} );
 
 
-		it( 'should return false if given range was not found in collection', () => {
-			const result = markers.updateRange( live, newLive );
+		it( 'should return true and use remove and add methods if range was found in collection - passed name', () => {
+			const newLive = LiveRange.createFromParentsAndOffsets( root, 1, root, 5 );
+			markers.add( 'name', live );
 
 
-			expect( result ).to.be.false;
+			sinon.spy( markers, 'remove' );
+			sinon.spy( markers, 'add' );
+
+			const result = markers.update( 'name', newLive );
+
+			expect( markers.remove.calledWith( 'name' ) ).to.be.true;
+			expect( markers.add.calledWith( 'name', newLive ) ).to.be.true;
+			expect( result ).to.be.true;
+
+			newLive.detach();
 		} );
 		} );
 
 
-		it( 'should return true and use removeRange and addRange methods if range was found in collection', () => {
+		it( 'should return true and use remove and add methods if range was found in collection - passed range', () => {
 			const newLive = LiveRange.createFromParentsAndOffsets( root, 1, root, 5 );
 			const newLive = LiveRange.createFromParentsAndOffsets( root, 1, root, 5 );
-			markers.addRange( live, 'name' );
+			markers.add( 'name', live );
 
 
-			sinon.spy( markers, 'removeRange' );
-			sinon.spy( markers, 'addRange' );
+			sinon.spy( markers, 'remove' );
+			sinon.spy( markers, 'add' );
 
 
-			const result = markers.updateRange( live, newLive );
+			const result = markers.update( live, newLive );
 
 
-			expect( markers.removeRange.calledWith( live ) ).to.be.true;
-			expect( markers.addRange.calledWith( newLive, 'name' ) ).to.be.true;
+			expect( markers.remove.calledWith( 'name' ) ).to.be.true;
+			expect( markers.add.calledWith( 'name', newLive ) ).to.be.true;
 			expect( result ).to.be.true;
 			expect( result ).to.be.true;
+
+			newLive.detach();
+		} );
+
+		it( 'should return false if given name was not found in collection', () => {
+			const result = markers.update( 'name', newLive );
+
+			expect( result ).to.be.false;
+		} );
+
+		it( 'should return false if given range was not found in collection', () => {
+			const result = markers.update( live, newLive );
+
+			expect( result ).to.be.false;
 		} );
 		} );
 	} );
 	} );