8
0
Просмотр исходного кода

Updated downcast converters to use view writer where possible.

Szymon Kupś 8 лет назад
Родитель
Сommit
d1348b47a7

+ 49 - 54
packages/ckeditor5-engine/src/conversion/downcast-converters.js

@@ -31,8 +31,6 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  *
  *
  *		downcastElementToElement( { model: 'paragraph', view: 'p' }, 'high' );
  *		downcastElementToElement( { model: 'paragraph', view: 'p' }, 'high' );
  *
  *
- *		downcastElementToElement( { model: 'paragraph', view: new ViewContainerElement( 'p' ) } );
- *
  *		downcastElementToElement( {
  *		downcastElementToElement( {
  *			model: 'fancyParagraph',
  *			model: 'fancyParagraph',
  *			view: {
  *			view: {
@@ -43,17 +41,23 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  *
  *
  * 		downcastElementToElement( {
  * 		downcastElementToElement( {
  * 			model: 'heading',
  * 			model: 'heading',
- * 			view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
+ * 			view: ( modelItem, consumable, conversionApi ) => {
+ * 				const viewWriter = conversionApi.writer;
+ *
+ * 				return viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
+ * 			}
  * 		} );
  * 		} );
  *
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
  *
  * @param {Object} config Conversion configuration.
  * @param {Object} config Conversion configuration.
  * @param {String} config.model Name of the model element to convert.
  * @param {String} config.model Name of the model element to convert.
- * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function|
- * module:engine/view/containerelement~ContainerElement} config.view View element name, or a view element definition,
- * or a function that takes model element as a parameter and returns a view container element,
- * or a view container element instance. The view element will be used then in conversion.
+ * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element name, or a
+ * view element definition, or a function that  will be provided with all the parameters of the dispatcher's
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}.
+ * It's expected that the function returns a {@link module:engine/view/containerelement~ContainerElement}.
+ * The view element will be used then in conversion.
+ *
  * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
  * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
@@ -77,8 +81,6 @@ export function downcastElementToElement( config, priority = 'normal' ) {
  *
  *
  *		downcastAttributeToElement( 'bold', { view: 'strong' }, 'high' );
  *		downcastAttributeToElement( 'bold', { view: 'strong' }, 'high' );
  *
  *
- *		downcastAttributeToElement( 'bold', { view: new ViewAttributeElement( 'strong' ) } );
- *
  *		downcastAttributeToElement( 'bold', {
  *		downcastAttributeToElement( 'bold', {
  *			view: {
  *			view: {
  *				name: 'span',
  *				name: 'span',
@@ -116,7 +118,11 @@ export function downcastElementToElement( config, priority = 'normal' ) {
  *		] );
  *		] );
  *
  *
  * 		downcastAttributeToElement( 'bold', {
  * 		downcastAttributeToElement( 'bold', {
- * 			view: modelAttributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } )
+ * 			view: ( modelAttributeValue, data, consumable, conversionApi ) => {
+ * 				const viewWriter = conversionApi.writer;
+ *
+ * 				return	viewWriter( 'span', { style: 'font-weight:' + modelAttributeValue } );
+ * 			}
  * 		} );
  * 		} );
  *
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
@@ -125,9 +131,8 @@ export function downcastElementToElement( config, priority = 'normal' ) {
  * @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
  * @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
  * @param {*} [config.model] The value of the converted model attribute for which the `view` property is defined.
  * @param {*} [config.model] The value of the converted model attribute for which the `view` property is defined.
  * If omitted, the configuration item will be used as a "default" configuration when no other item matches the attribute value.
  * If omitted, the configuration item will be used as a "default" configuration when no other item matches the attribute value.
- * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function|
- * module:engine/view/attributeelement~AttributeElement} config.view View element name, or a view element definition,
- * or a function that takes model element as a parameter and returns a view attribute element, or a view attribute element instance.
+ * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element name,
+ * or a view element definition, or a function that takes model element as a parameter and returns a view attribute element.
  * The view element will be used then in conversion.
  * The view element will be used then in conversion.
  * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
  * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
@@ -246,14 +251,14 @@ export function downcastAttributeToAttribute( modelAttributeKey, config = {}, pr
  *
  *
  * 		downcastMarkerToElement( {
  * 		downcastMarkerToElement( {
  * 			model: 'search',
  * 			model: 'search',
- * 			view: data => {
- *	 			return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
+ * 			view: ( data, conversionApi ) => {
+ *	 			return conversionApi.writer.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
  * 			}
  * 			}
  * 		} );
  * 		} );
  *
  *
  * If function is passed as `config.view` parameter, it will be used to generate both boundary elements. The function
  * If function is passed as `config.view` parameter, it will be used to generate both boundary elements. The function
  * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
  * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
- * The `data` object properties are passed from
+ * The `data` and `conversionApi` objects are passed from
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
  * `data.isOpening` parameter is passed, which is set to `true` for marker start boundary element, and `false` to
  * `data.isOpening` parameter is passed, which is set to `true` for marker start boundary element, and `false` to
  * marker end boundary element.
  * marker end boundary element.
@@ -317,8 +322,8 @@ export function downcastMarkerToElement( config, priority = 'normal' ) {
  * 		} );
  * 		} );
  *
  *
  * If function is passed as `config.view` parameter, it will be used to generate highlight descriptor. The function
  * If function is passed as `config.view` parameter, it will be used to generate highlight descriptor. The function
- * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
- * The `data` object properties are passed from
+ * receives `data` and `conversionApi` objects as parameters and should return
+ * {@link module:engine/conversion/downcast-converters~HighlightDescriptor}. The `data` and `conversionApi` objects are passed from
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
  *
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
@@ -353,17 +358,19 @@ function _normalizeToElementConfig( config, ViewElementClass ) {
 		return;
 		return;
 	}
 	}
 
 
+	const view = config.view;
+
 	// Build `.view` property.
 	// Build `.view` property.
-	// It is expected to be either creator function or view element instance.
-	if ( typeof config.view == 'string' ) {
-		// If `.view` is a string, create a proper view element instance out of given `ViewElementClass` and name given in `.view`.
-		config.view = new ViewElementClass( config.view );
-	} else if ( typeof config.view == 'object' && !( config.view instanceof ViewElementClass ) ) {
+	// It is expected to be either string, element definition or creator function.
+	if ( typeof view == 'string' ) {
+		// If `.view` is a string, create a function that returns view element instance out of given `ViewElementClass`.
+		config.view = () => new ViewElementClass( view );
+	} else if ( typeof view == 'object' ) {
 		// If `.view` is an object, use it to build view element instance.
 		// If `.view` is an object, use it to build view element instance.
-		config.view = _createViewElementFromDefinition( config.view, ViewElementClass );
+		const element = _createViewElementFromDefinition( view, ViewElementClass );
+		config.view = () => element.clone();
 	}
 	}
-	// `.view` can be also a function or already a view element instance.
-	// These are already valid types which don't have to be normalized.
+	// `.view` can be also a function that is already valid type which don't have to be normalized.
 }
 }
 
 
 // Creates view element instance from provided viewElementDefinition and class.
 // Creates view element instance from provided viewElementDefinition and class.
@@ -458,12 +465,8 @@ function _getCreatorForArrayConfig( config ) {
 
 
 		// If there was default config or matched config...
 		// If there was default config or matched config...
 		if ( matchedConfigEntry ) {
 		if ( matchedConfigEntry ) {
-			// If the entry `.view` is a function, execute it and return the value...
-			if ( typeof matchedConfigEntry.view == 'function' ) {
-				return matchedConfigEntry.view( modelAttributeValue );
-			}
-			// Else, just return `.view`, it should be a view element instance after it got normalized earlier.
-			return matchedConfigEntry.view;
+			// The entry `.view` is a function after it got normalized earlier, execute it and return the value.
+			return matchedConfigEntry.view( modelAttributeValue );
 		}
 		}
 
 
 		return null;
 		return null;
@@ -472,21 +475,20 @@ function _getCreatorForArrayConfig( config ) {
 
 
 /**
 /**
  * Function factory, creates a converter that converts node insertion changes from the model to the view.
  * Function factory, creates a converter that converts node insertion changes from the model to the view.
- * The view element that will be added to the view depends on passed parameter. If {@link module:engine/view/element~Element} was passed,
- * it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the
- * dispatcher's {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}.
+ * Passed function will be provided with all the parameters of the dispatcher's
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}.
  * It's expected that the function returns a {@link module:engine/view/element~Element}.
  * It's expected that the function returns a {@link module:engine/view/element~Element}.
  * The result of the function will be inserted to the view.
  * The result of the function will be inserted to the view.
  *
  *
  * 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/downcastdispatcher~DowncastDispatcher}) and bind model and view elements.
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and bind model and view elements.
  *
  *
- *		modelDispatcher.on( 'insert:paragraph', insertElement( new ViewElement( 'p' ) ) );
- *
  *		modelDispatcher.on(
  *		modelDispatcher.on(
  *			'insert:myElem',
  *			'insert:myElem',
  *			insertElement( ( modelItem, consumable, conversionApi ) => {
  *			insertElement( ( modelItem, consumable, conversionApi ) => {
- *				let myElem = new ViewElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, new ViewText( 'myText' ) );
+ *				const writer = conversionApi.writer;
+ *				const text = writer.createText( 'myText' );
+ *				const myElem = writer.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
  *
  *
  *				// Do something fancy with myElem using `modelItem` or other parameters.
  *				// Do something fancy with myElem using `modelItem` or other parameters.
  *
  *
@@ -494,15 +496,12 @@ function _getCreatorForArrayConfig( config ) {
  *			}
  *			}
  *		) );
  *		) );
  *
  *
- * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which
- * will be inserted.
+ * @param {Function} elementCreator Function returning a view element, which will be inserted.
  * @returns {Function} Insert element event converter.
  * @returns {Function} Insert element event converter.
  */
  */
 export function insertElement( elementCreator ) {
 export function insertElement( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
 	return ( evt, data, consumable, conversionApi ) => {
-		const viewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.item, consumable, conversionApi );
+		const viewElement = elementCreator( data.item, consumable, conversionApi );
 
 
 		if ( !viewElement ) {
 		if ( !viewElement ) {
 			return;
 			return;
@@ -751,8 +750,7 @@ export function changeAttribute( attributeCreator ) {
  *			|- b {bold: true}                             |   |- ab
  *			|- b {bold: true}                             |   |- ab
  *			|- c                                          |- c
  *			|- c                                          |- c
  *
  *
- * The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and
- * the copy will become the wrapping element. If `Function` is provided, it is passed attribute value and then all the parameters of the
+ * Passed `Function` will be provided with attribute value and then all the parameters of the
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute event}.
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute event}.
  * It's expected that the function returns a {@link module:engine/view/element~Element}.
  * It's expected that the function returns a {@link module:engine/view/element~Element}.
  * The result of the function will be the wrapping element.
  * The result of the function will be the wrapping element.
@@ -761,24 +759,21 @@ export function changeAttribute( 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/downcastdispatcher~DowncastDispatcher}).
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
  *
  *
- *		modelDispatcher.on( 'attribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
+ *		modelDispatcher.on( 'attribute:bold', wrapItem( ( attributeValue, data, consumable, conversionApi ) => {
+ *			return conversionApi.writer.createAttributeElement( 'strong' );
+ *		} );
  *
  *
- * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
- * be used for wrapping.
+ * @param {Function} elementCreator Function returning a view element, which will be used for wrapping.
  * @returns {Function} Set/change attribute converter.
  * @returns {Function} Set/change attribute converter.
  */
  */
 export function wrap( elementCreator ) {
 export function wrap( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
 	return ( evt, data, consumable, conversionApi ) => {
 		// Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
 		// Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
 		// or the attribute was removed.
 		// or the attribute was removed.
-		const oldViewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.attributeOldValue, data, consumable, conversionApi );
+		const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi );
 
 
 		// Create node to wrap with.
 		// Create node to wrap with.
-		const newViewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.attributeNewValue, data, consumable, conversionApi );
+		const newViewElement = elementCreator( data.attributeNewValue, data, consumable, conversionApi );
 
 
 		if ( !oldViewElement && !newViewElement ) {
 		if ( !oldViewElement && !newViewElement ) {
 			return;
 			return;

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

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

+ 17 - 56
packages/ckeditor5-engine/tests/conversion/downcast-converters.js

@@ -73,21 +73,6 @@ describe( 'downcast-helpers', () => {
 			expectResult( '<foo></foo>' );
 			expectResult( '<foo></foo>' );
 		} );
 		} );
 
 
-		it( 'config.view is an element instance', () => {
-			const helper = downcastElementToElement( {
-				model: 'paragraph',
-				view: new ViewContainerElement( 'p' )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertElement( 'paragraph', modelRoot, 0 );
-			} );
-
-			expectResult( '<p></p>' );
-		} );
-
 		it( 'config.view is a view element definition', () => {
 		it( 'config.view is a view element definition', () => {
 			const helper = downcastElementToElement( {
 			const helper = downcastElementToElement( {
 				model: 'fancyParagraph',
 				model: 'fancyParagraph',
@@ -148,20 +133,6 @@ describe( 'downcast-helpers', () => {
 			expectResult( '<b>foo</b>' );
 			expectResult( '<b>foo</b>' );
 		} );
 		} );
 
 
-		it( 'config.view is an element instance', () => {
-			const helper = downcastAttributeToElement( 'bold', {
-				view: new ViewAttributeElement( 'strong' )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
-			} );
-
-			expectResult( '<strong>foo</strong>' );
-		} );
-
 		it( 'config.view is a view element definition', () => {
 		it( 'config.view is a view element definition', () => {
 			const helper = downcastAttributeToElement( 'bold', {
 			const helper = downcastAttributeToElement( 'bold', {
 				view: {
 				view: {
@@ -421,22 +392,6 @@ describe( 'downcast-helpers', () => {
 			expectResult( 'f<search></search>o<search></search>o' );
 			expectResult( 'f<search></search>o<search></search>o' );
 		} );
 		} );
 
 
-		it( 'config.view is an element instance', () => {
-			const helper = downcastMarkerToElement( {
-				model: 'search',
-				view: new ViewUIElement( 'span', { 'data-marker': 'search' } )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', modelRoot, 0 );
-				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
-			} );
-
-			expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
-		} );
-
 		it( 'config.view is a view element definition', () => {
 		it( 'config.view is a view element definition', () => {
 			const helper = downcastMarkerToElement( {
 			const helper = downcastMarkerToElement( {
 				model: 'search',
 				model: 'search',
@@ -550,7 +505,13 @@ describe( 'downcast-converters', () => {
 
 
 		dispatcher = controller.downcastDispatcher;
 		dispatcher = controller.downcastDispatcher;
 
 
-		dispatcher.on( 'insert:paragraph', insertElement( () => new ViewContainerElement( 'p' ) ) );
+		dispatcher.on(
+			'insert:paragraph',
+			insertElement(
+				( modelItem, consumable, conversionApi ) => conversionApi.writer.createContainerElement( 'p' )
+			)
+		);
+
 		dispatcher.on( 'attribute:class', changeAttribute() );
 		dispatcher.on( 'attribute:class', changeAttribute() );
 
 
 		modelRootStart = ModelPosition.createAt( modelRoot, 0 );
 		modelRootStart = ModelPosition.createAt( modelRoot, 0 );
@@ -685,7 +646,7 @@ describe( 'downcast-converters', () => {
 				return { key: 'class', value };
 				return { key: 'class', value };
 			};
 			};
 
 
-			dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
+			dispatcher.on( 'insert:div', insertElement( ( model, consumable, api ) => api.writer.createContainerElement( 'div' ) ) );
 			dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) );
 			dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) );
 
 
 			const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
 			const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
@@ -729,9 +690,9 @@ describe( 'downcast-converters', () => {
 	describe( 'wrap', () => {
 	describe( 'wrap', () => {
 		it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
 		it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
-			const viewB = new ViewAttributeElement( 'b' );
+			const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' );
 
 
-			dispatcher.on( 'attribute:bold', wrap( viewB ) );
+			dispatcher.on( 'attribute:bold', wrap( creator ) );
 
 
 			model.change( writer => {
 			model.change( writer => {
 				writer.insert( modelElement, modelRootStart );
 				writer.insert( modelElement, modelRootStart );
@@ -749,9 +710,9 @@ describe( 'downcast-converters', () => {
 		it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
 		it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
 
 
-			const elementGenerator = value => {
+			const elementGenerator = ( value, data, consumable, api ) => {
 				if ( value == 'bold' ) {
 				if ( value == 'bold' ) {
-					return new ViewAttributeElement( 'b' );
+					return api.writer.createAttributeElement( 'b' );
 				}
 				}
 			};
 			};
 
 
@@ -777,7 +738,7 @@ describe( 'downcast-converters', () => {
 				new ModelText( 'x' )
 				new ModelText( 'x' )
 			] );
 			] );
 
 
-			const elementGenerator = href => new ViewAttributeElement( 'a', { href } );
+			const elementGenerator = ( href, data, consumable, api ) => api.writer.createAttributeElement( 'a', { href } );
 
 
 			dispatcher.on( 'attribute:link', wrap( elementGenerator ) );
 			dispatcher.on( 'attribute:link', wrap( elementGenerator ) );
 
 
@@ -797,9 +758,9 @@ describe( 'downcast-converters', () => {
 
 
 		it( 'should support unicode', () => {
 		it( 'should support unicode', () => {
 			const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
 			const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
-			const viewB = new ViewAttributeElement( 'b' );
+			const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' );
 
 
-			dispatcher.on( 'attribute:bold', wrap( viewB ) );
+			dispatcher.on( 'attribute:bold', wrap( creator ) );
 
 
 			model.change( writer => {
 			model.change( writer => {
 				writer.insert( modelElement, modelRootStart );
 				writer.insert( modelElement, modelRootStart );
@@ -816,9 +777,9 @@ describe( 'downcast-converters', () => {
 
 
 		it( 'should be possible to override wrap', () => {
 		it( 'should be possible to override wrap', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
-			const viewB = new ViewAttributeElement( 'b' );
+			const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' );
 
 
-			dispatcher.on( 'attribute:bold', wrap( viewB ) );
+			dispatcher.on( 'attribute:bold', wrap( creator ) );
 			dispatcher.on( 'attribute:bold', ( evt, data, consumable ) => {
 			dispatcher.on( 'attribute:bold', ( evt, data, consumable ) => {
 				consumable.consume( data.item, 'attribute:bold' );
 				consumable.consume( data.item, 'attribute:bold' );
 			}, { priority: 'high' } );
 			}, { priority: 'high' } );