/**
 * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module engine/conversion/upcastdispatcher
 */

import ViewConsumable from './viewconsumable';
import ModelRange from '../model/range';
import ModelPosition from '../model/position';
import { SchemaContext } from '../model/schema';

import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';

/**
 * `UpcastDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
 * converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
 * {@link module:engine/view/element~Element} into another structure.
 * In default application, {@link module:engine/view/view view} is converted to {@link module:engine/model/model}.
 *
 * During conversion process, for all {@link module:engine/view/node~Node view nodes} from the converted view document fragment,
 * `UpcastDispatcher` fires corresponding events. Special callbacks called "converters" should listen to
 * `UpcastDispatcher` for those events.
 *
 * Each callback, as the second argument, is passed a special object `data` that has `viewItem`, `modelCursor` and
 * `modelRange` properties. `viewItem` property contains {@link module:engine/view/node~Node view node} or
 * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
 * that is converted at the moment and might be handled by the callback. `modelRange` property should be used to save the result
 * of conversion and is always a {@link module:engine/model/range~Range} when conversion result is correct.
 * `modelCursor` property is a {@link module:engine/model/position~Position position} on which conversion result will be inserted
 * and is a context according to {@link module:engine/model/schema~Schema schema} will be checked before the conversion.
 * See also {@link ~UpcastDispatcher#convert}. It is also shared by reference by all callbacks listening to given event.
 *
 * The third parameter passed to a callback is an instance of {@link ~UpcastDispatcher}
 * which provides additional tools for converters.
 *
 * Examples of providing callbacks for `UpcastDispatcher`:
 *
 *		// Converter for links (<a>).
 *		editor.data.upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
 *			if ( conversionApi.consumable.consume( data.viewItem, { name: true, attributes: [ 'href' ] } ) ) {
 *				// <a> element is inline and is represented by an attribute in the model.
 *				// This is why we need to convert only children.
 *				const { modelRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
 *
 *				for ( let item of modelRange.getItems() ) {
 *					if ( conversionApi.schema.checkAttribute( item, 'linkHref' ) ) {
 *						conversionApi.writer.setAttribute( 'linkHref', data.viewItem.getAttribute( 'href' ), item );
 *					}
 *				}
 *			}
 *		} );
 *
 *		// Convert <p>'s font-size style.
 *		// Note: You should use a low-priority observer in order to ensure that
 *		// it's executed after the element-to-element converter.
 *		editor.data.upcastDispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
 *			const { consumable, schema, writer } = conversionApi;
 *
 *			if ( !consumable.consume( data.viewItem, { style: 'font-size' } ) ) {
 *				return;
 *			}
 *
 *			const fontSize = data.viewItem.getStyle( 'font-size' );
 *
 *			// Don't go for the model element after data.modelCursor because it might happen
 *			// that a single view element was converted to multiple model elements. Get all of them.
 *			for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
 *				if ( schema.checkAttribute( item, 'fontSize' ) ) {
 *					writer.setAttribute( 'fontSize', fontSize, item );
 *				}
 *			}
 *		}, { priority: 'low' } );
 *
 *		// Convert all elements which have no custom converter into paragraph (autoparagraphing).
 *  	editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
 *  	 	// When element is already consumed by higher priority converters then do nothing.
 *  	 	if ( conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
 *  	 			const paragraph = conversionApi.writer.createElement( 'paragraph' );
 *
 *  	 			// Find allowed parent for paragraph that we are going to insert. If current parent does not allow
 *  	 			// to insert paragraph but one of the ancestors does then split nodes to allowed parent.
 *  	 			const splitResult = conversionApi.splitToAllowedParent( paragraph, data.modelCursor );
 *
 *  	 			// When there is no split result it means that we can't insert paragraph in this position.
 *  	 			if ( splitResult ) {
 *  	 				// Insert paragraph in allowed position.
 *  	 				conversionApi.writer.insert( paragraph, splitResult.position );
 *
 *  	 				// Convert children to paragraph.
 *  	 				const { modelRange } = conversionApi.convertChildren(
 *  	 					data.viewItem,
 *  	 					conversionApi.writer.createPositionAt( paragraph, 0 )
 *  	 				);
 *
 * 						// Set as conversion result, attribute converters may use this property.
 *  	 				data.modelRange = conversionApi.writer.createRange(
 *  	 					conversionApi.writer.createPositionBefore( paragraph ),
 *  	 					modelRange.end
 *  	 				);
 *
 *  	 				// Continue conversion inside paragraph.
 *  	 				data.modelCursor = data.modelRange.end;
 *  	 			}
 *  	 		}
 *  	 	}
 *  	 }, { priority: 'low' } );
 *
 * Before each conversion process, `UpcastDispatcher` fires {@link ~UpcastDispatcher#event:viewCleanup}
 * event which can be used to prepare tree view for conversion.
 *
 * @mixes module:utils/emittermixin~EmitterMixin
 * @fires viewCleanup
 * @fires element
 * @fires text
 * @fires documentFragment
 */
export default class UpcastDispatcher {
	/**
	 * Creates a `UpcastDispatcher` that operates using passed API.
	 *
	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi
	 * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
	 * by `UpcastDispatcher`.
	 */
	constructor( conversionApi = {} ) {
		/**
		 * List of the elements that were created during splitting.
		 *
		 * After conversion process the list is cleared.
		 *
		 * @private
		 * @type {Map.<module:engine/model/element~Element,Array.<module:engine/model/element~Element>>}
		 */
		this._splitParts = new Map();

		/**
		 * Position in the temporary structure where the converted content is inserted. The structure reflect the context of
		 * the target position where the content will be inserted. This property is build based on the context parameter of the
		 * convert method.
		 *
		 * @private
		 * @type {module:engine/model/position~Position|null}
		 */
		this._modelCursor = null;

		/**
		 * Interface passed by dispatcher to the events callbacks.
		 *
		 * @member {module:engine/conversion/upcastdispatcher~UpcastConversionApi}
		 */
		this.conversionApi = Object.assign( {}, conversionApi );

		// `convertItem`, `convertChildren` and `splitToAllowedParent` are bound to this `UpcastDispatcher`
		// instance and set on `conversionApi`. This way only a part of `UpcastDispatcher` API is exposed.
		this.conversionApi.convertItem = this._convertItem.bind( this );
		this.conversionApi.convertChildren = this._convertChildren.bind( this );
		this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
		this.conversionApi.getSplitParts = this._getSplitParts.bind( this );
	}

	/**
	 * Starts the conversion process. The entry point for the conversion.
	 *
	 * @fires element
	 * @fires text
	 * @fires documentFragment
	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
	 * Part of the view to be converted.
	 * @param {module:engine/model/writer~Writer} writer Instance of model writer.
	 * @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Elements will be converted according to this context.
	 * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process
	 * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
	 * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
	 */
	convert( viewItem, writer, context = [ '$root' ] ) {
		this.fire( 'viewCleanup', viewItem );

		// Create context tree and set position in the top element.
		// Items will be converted according to this position.
		this._modelCursor = createContextTree( context, writer );

		// Store writer in conversion as a conversion API
		// to be sure that conversion process will use the same batch.
		this.conversionApi.writer = writer;

		// Create consumable values list for conversion process.
		this.conversionApi.consumable = ViewConsumable.createFrom( viewItem );

		// Custom data stored by converter for conversion process.
		this.conversionApi.store = {};

		// Do the conversion.
		const { modelRange } = this._convertItem( viewItem, this._modelCursor );

		// Conversion result is always a document fragment so let's create it.
		const documentFragment = writer.createDocumentFragment();

		// When there is a conversion result.
		if ( modelRange ) {
			// Remove all empty elements that were create while splitting.
			this._removeEmptyElements();

			// Move all items that were converted in context tree to the document fragment.
			for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) {
				writer.append( item, documentFragment );
			}

			// Extract temporary markers elements from model and set as static markers collection.
			documentFragment.markers = extractMarkersFromModelFragment( documentFragment, writer );
		}

		// Clear context position.
		this._modelCursor = null;

		// Clear split elements lists.
		this._splitParts.clear();

		// Clear conversion API.
		this.conversionApi.writer = null;
		this.conversionApi.store = null;

		// Return fragment as conversion result.
		return documentFragment;
	}

	/**
	 * @private
	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertItem
	 */
	_convertItem( viewItem, modelCursor ) {
		const data = Object.assign( { viewItem, modelCursor, modelRange: null } );

		if ( viewItem.is( 'element' ) ) {
			this.fire( 'element:' + viewItem.name, data, this.conversionApi );
		} else if ( viewItem.is( 'text' ) ) {
			this.fire( 'text', data, this.conversionApi );
		} else {
			this.fire( 'documentFragment', data, this.conversionApi );
		}

		// Handle incorrect conversion result.
		if ( data.modelRange && !( data.modelRange instanceof ModelRange ) ) {
			/**
			 * Incorrect conversion result was dropped.
			 *
			 * {@link module:engine/model/range~Range Model range} should be a conversion result.
			 *
			 * @error view-conversion-dispatcher-incorrect-result
			 */
			throw new CKEditorError( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.', this );
		}

		return { modelRange: data.modelRange, modelCursor: data.modelCursor };
	}

	/**
	 * @private
	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertChildren
	 */
	_convertChildren( viewItem, modelCursor ) {
		const modelRange = new ModelRange( modelCursor );
		let nextModelCursor = modelCursor;

		for ( const viewChild of Array.from( viewItem.getChildren() ) ) {
			const result = this._convertItem( viewChild, nextModelCursor );

			if ( result.modelRange instanceof ModelRange ) {
				modelRange.end = result.modelRange.end;
				nextModelCursor = result.modelCursor;
			}
		}

		return { modelRange, modelCursor: nextModelCursor };
	}

	/**
	 * @private
	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#splitToAllowedParent
	 */
	_splitToAllowedParent( node, modelCursor ) {
		// Try to find allowed parent.
		const allowedParent = this.conversionApi.schema.findAllowedParent( modelCursor, node );

		// When there is no parent that allows to insert node then return `null`.
		if ( !allowedParent ) {
			return null;
		}

		// When current position parent allows to insert node then return this position.
		if ( allowedParent === modelCursor.parent ) {
			return { position: modelCursor };
		}

		// When allowed parent is in context tree.
		if ( this._modelCursor.parent.getAncestors().includes( allowedParent ) ) {
			return null;
		}

		// Split element to allowed parent.
		const splitResult = this.conversionApi.writer.split( modelCursor, allowedParent );

		// Using the range returned by `model.Writer#split`, we will pair original elements with their split parts.
		//
		// The range returned from the writer spans "over the split" or, precisely saying, from the end of the original element (the one
		// that got split) to the beginning of the other part of that element:
		//
		// <limit><a><b><c>X[]Y</c></b><a></limit> ->
		// <limit><a><b><c>X[</c></b></a><a><b><c>]Y</c></b></a>
		//
		// After the split there cannot be any full node between the positions in `splitRange`. The positions are touching.
		// Also, because of how splitting works, it is easy to notice, that "closing tags" are in the reverse order than "opening tags".
		// Also, since we split all those elements, each of them has to have the other part.
		//
		// With those observations in mind, we will pair the original elements with their split parts by saving "closing tags" and matching
		// them with "opening tags" in the reverse order. For that we can use a stack.
		const stack = [];

		for ( const treeWalkerValue of splitResult.range.getWalker() ) {
			if ( treeWalkerValue.type == 'elementEnd' ) {
				stack.push( treeWalkerValue.item );
			} else {
				// There should not be any text nodes after the element is split, so the only other value is `elementStart`.
				const originalPart = stack.pop();
				const splitPart = treeWalkerValue.item;

				this._registerSplitPair( originalPart, splitPart );
			}
		}

		return {
			position: splitResult.position,
			cursorParent: splitResult.range.end.parent
		};
	}

	/**
	 * Registers that `splitPart` element is a split part of the `originalPart` element.
	 *
	 * Data set by this method is used by {@link #_getSplitParts} and {@link #_removeEmptyElements}.
	 *
	 * @private
	 * @param {module:engine/model/element~Element} originalPart
	 * @param {module:engine/model/element~Element} splitPart
	 */
	_registerSplitPair( originalPart, splitPart ) {
		if ( !this._splitParts.has( originalPart ) ) {
			this._splitParts.set( originalPart, [ originalPart ] );
		}

		const list = this._splitParts.get( originalPart );

		this._splitParts.set( splitPart, list );
		list.push( splitPart );
	}

	/**
	 * @private
	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#getSplitParts
	 */
	_getSplitParts( element ) {
		let parts;

		if ( !this._splitParts.has( element ) ) {
			parts = [ element ];
		} else {
			parts = this._splitParts.get( element );
		}

		return parts;
	}

	/**
	 * Checks if there are any empty elements created while splitting and removes them.
	 *
	 * This method works recursively to re-check empty elements again after at least one element was removed in the initial call,
	 * as some elements might have become empty after other empty elements were removed from them.
	 *
	 * @private
	 */
	_removeEmptyElements() {
		let anyRemoved = false;

		for ( const element of this._splitParts.keys() ) {
			if ( element.isEmpty ) {
				this.conversionApi.writer.remove( element );
				this._splitParts.delete( element );

				anyRemoved = true;
			}
		}

		if ( anyRemoved ) {
			this._removeEmptyElements();
		}
	}

	/**
	 * Fired before the first conversion event, at the beginning of upcast (view to model conversion) process.
	 *
	 * @event viewCleanup
	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
	 * viewItem Part of the view to be converted.
	 */

	/**
	 * Fired when {@link module:engine/view/element~Element} is converted.
	 *
	 * `element` is a namespace event for a class of events. Names of actually called events follow this pattern:
	 * `element:<elementName>` where `elementName` is the name of converted element. This way listeners may listen to
	 * all elements conversion or to conversion of specific elements.
	 *
	 * @event element
	 * @param {Object} data Conversion data. Keep in mind that this object is shared by reference between all
	 * callbacks that will be called. This means that callbacks can override values if needed, and those values will
	 * be available in other callbacks.
	 * @param {module:engine/view/item~Item} data.viewItem Converted item.
	 * @param {module:engine/model/position~Position} data.modelCursor Position where a converter should start changes.
	 * Change this value for the next converter to tell where the conversion should continue.
	 * @param {module:engine/model/range~Range} data.modelRange The current state of conversion result. Every change to
	 * converted element should be reflected by setting or modifying this property.
	 * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion utilities to be used by callback.
	 */

	/**
	 * Fired when {@link module:engine/view/text~Text} is converted.
	 *
	 * @event text
	 * @see #event:element
	 */

	/**
	 * Fired when {@link module:engine/view/documentfragment~DocumentFragment} is converted.
	 *
	 * @event documentFragment
	 * @see #event:element
	 */
}

mix( UpcastDispatcher, EmitterMixin );

// Traverses given model item and searches elements which marks marker range. Found element is removed from
// DocumentFragment but path of this element is stored in a Map which is then returned.
//
// @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
// @returns {Map<String, module:engine/model/range~Range>} List of static markers.
function extractMarkersFromModelFragment( modelItem, writer ) {
	const markerElements = new Set();
	const markers = new Map();

	// Create ModelTreeWalker.
	const range = ModelRange._createIn( modelItem ).getItems();

	// Walk through DocumentFragment and collect marker elements.
	for ( const item of range ) {
		// Check if current element is a marker.
		if ( item.name == '$marker' ) {
			markerElements.add( item );
		}
	}

	// Walk through collected marker elements store its path and remove its from the DocumentFragment.
	for ( const markerElement of markerElements ) {
		const markerName = markerElement.getAttribute( 'data-name' );
		const currentPosition = writer.createPositionBefore( markerElement );

		// When marker of given name is not stored it means that we have found the beginning of the range.
		if ( !markers.has( markerName ) ) {
			markers.set( markerName, new ModelRange( currentPosition.clone() ) );
		// Otherwise is means that we have found end of the marker range.
		} else {
			markers.get( markerName ).end = currentPosition.clone();
		}

		// Remove marker element from DocumentFragment.
		writer.remove( markerElement );
	}

	return markers;
}

// Creates model fragment according to given context and returns position in the bottom (the deepest) element.
function createContextTree( contextDefinition, writer ) {
	let position;

	for ( const item of new SchemaContext( contextDefinition ) ) {
		const attributes = {};

		for ( const key of item.getAttributeKeys() ) {
			attributes[ key ] = item.getAttribute( key );
		}

		const current = writer.createElement( item.name, attributes );

		if ( position ) {
			writer.append( current, position );
		}

		position = ModelPosition._createAt( current, 0 );
	}

	return position;
}

/**
 * Conversion interface that is registered for given {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}
 * and is passed as one of parameters when {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher dispatcher}
 * fires it's events.
 *
 * @interface module:engine/conversion/upcastdispatcher~UpcastConversionApi
 */

/**
 * Starts conversion of given item by firing an appropriate event.
 *
 * Every fired event is passed (as first parameter) an object with `modelRange` property. Every event may set and/or
 * modify that property. When all callbacks are done, the final value of `modelRange` property is returned by this method.
 * The `modelRange` must be {@link module:engine/model/range~Range model range} or `null` (as set by default).
 *
 * @method #convertItem
 * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
 * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
 * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
 * @param {module:engine/view/item~Item} viewItem Item to convert.
 * @param {module:engine/model/position~Position} modelCursor Position of conversion.
 * @returns {Object} result Conversion result.
 * @returns {module:engine/model/range~Range|null} result.modelRange Model range containing result of item conversion,
 * created and modified by callbacks attached to fired event, or `null` if the conversion result was incorrect.
 * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
 */

/**
 * Starts conversion of all children of given item by firing appropriate events for all those children.
 *
 * @method #convertChildren
 * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
 * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
 * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
 * @param {module:engine/view/item~Item} viewItem Element which children should be converted.
 * @param {module:engine/model/position~Position} modelCursor Position of conversion.
 * @returns {Object} result Conversion result.
 * @returns {module:engine/model/range~Range} result.modelRange Model range containing results of conversion of all children of given item.
 * When no children was converted then range is collapsed.
 * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
 */

/**
 * Checks {@link module:engine/model/schema~Schema schema} to find allowed parent for element that we are going to insert
 * starting from given position. If current parent does not allow to insert element but one of the ancestors does then
 * split nodes to allowed parent.
 *
 * If schema allows to insert node in given position, nothing is split and object with that position is returned.
 *
 * If it was not possible to find allowed parent, `null` is returned, nothing is split.
 *
 * Otherwise, ancestors are split and object with position and the copy of the split element is returned.
 *
 * For instance, if `<image>` is not allowed in `<paragraph>` but is allowed in `$root`:
 *
 *		<paragraph>foo[]bar</paragraph>
 *
 *  	-> split for `<image>` ->
 *
 *  	<paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
 *
 * In the sample above position between `<paragraph>` elements will be returned as `position` and the second `paragraph`
 * as `cursorParent`.
 *
 * @method #splitToAllowedParent
 * @param {module:engine/model/position~Position} position Position on which element is going to be inserted.
 * @param {module:engine/model/node~Node} node Node to insert.
 * @returns {Object|null} Split result. If it was not possible to find allowed position `null` is returned.
 * @returns {module:engine/model/position~Position} position between split elements.
 * @returns {module:engine/model/element~Element} [cursorParent] Element inside which cursor should be placed to
 * continue conversion. When element is not defined it means that there was no split.
 */

/**
 * Returns all the split parts of given `element` that were created during upcasting through using {@link #splitToAllowedParent}.
 * It enables you to easily track those elements and continue processing them after they are split during their children conversion.
 *
 *		<paragraph>Foo<image />bar<image />baz</paragraph> ->
 *		<paragraph>Foo</paragraph><image /><paragraph>bar</paragraph><image /><paragraph>baz</paragraph>
 *
 * For a reference to any of above paragraphs, the function will return all three paragraphs (the original element included),
 * sorted in the order of their creation (the original element is the first one).
 *
 * If given `element` was not split, an array with single element is returned.
 *
 * Example of a usage in a converter code:
 *
 *		const myElement = conversionApi.writer.createElement( 'myElement' );
 *
 *		// Children conversion may split `myElement`.
 *		conversionApi.convertChildren( myElement, modelCursor );
 *
 *		const splitParts = conversionApi.getSplitParts( myElement );
 *		const lastSplitPart = splitParts[ splitParts.length - 1 ];
 *
 *		// Setting `data.modelRange` basing on split parts:
 *		data.modelRange = conversionApi.writer.createRange(
 *			conversionApi.writer.createPositionBefore( myElement ),
 *			conversionApi.writer.createPositionAfter( lastSplitPart )
 *		);
 *
 *		// Setting `data.modelCursor` to continue after the last split element:
 *		data.modelCursor = conversionApi.writer.createPositionAfter( lastSplitPart );
 *
 * **Tip:** if you are unable to get a reference to the original element (for example because the code is split into multiple converters
 * or even classes) but it was already converted, you might want to check first element in `data.modelRange`. This is a common situation
 * if an attribute converter is separated from an element converter.
 *
 * @method #getSplitParts
 * @param {module:engine/model/element~Element} element
 * @returns {Array.<module:engine/model/element~Element>}
 */

/**
 * Stores information about what parts of processed view item are still waiting to be handled. After a piece of view item
 * was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
 *
 * @member {module:engine/conversion/viewconsumable~ViewConsumable} #consumable
 */

/**
 * Custom data stored by converters for conversion process. Custom properties of this object can be defined and use to
 * pass parameters between converters.
 *
 * The difference between this property and `data` parameter of
 * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element} is that `data` parameters allows you
 * to pass parameters within a single event and `store` within the whole conversion.
 *
 * @member {Object} #store
 */

/**
 * The model's schema instance.
 *
 * @member {module:engine/model/schema~Schema} #schema
 */

/**
 * The {@link module:engine/model/writer~Writer} instance used to manipulate data during conversion.
 *
 * @member {module:engine/model/writer~Writer} #writer
 */
