Przeglądaj źródła

Merge branch 'master' into t/1208

Maciej Bukowski 8 lat temu
rodzic
commit
ab833c5bb8

+ 0 - 1
packages/ckeditor5-engine/.travis.yml

@@ -21,7 +21,6 @@ install:
 script:
 - ckeditor5-dev-tests-travis
 after_success:
-- codeclimate-test-reporter < coverage/lcov.info
 - ckeditor5-dev-tests-save-revision
 env:
   global:

+ 2 - 1
packages/ckeditor5-engine/README.md

@@ -5,7 +5,8 @@ CKEditor 5 editing engine
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-engine.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-engine.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-engine)
 [![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://www.browserstack.com/automate/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
-[![Test Coverage](https://codeclimate.com/github/ckeditor/ckeditor5-engine/badges/coverage.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-engine/coverage)
+[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-engine/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-engine?branch=master)
+<br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-engine/status.svg)](https://david-dm.org/ckeditor/ckeditor5-engine)
 [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-engine/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-engine?type=dev)
 

+ 344 - 0
packages/ckeditor5-engine/src/conversion/definition-based-converters.js

@@ -0,0 +1,344 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/conversion/definition-based-converters
+ */
+
+import AttributeElement from '../view/attributeelement';
+import ViewContainerElement from '../view/containerelement';
+
+import buildModelConverter from './buildmodelconverter';
+import buildViewConverter from './buildviewconverter';
+
+/**
+ * Helper for creating a model element to {@link module:engine/view/containerelement~ContainerElement view container element}
+ * converters.
+ *
+ * You can create a converter by using a simplified converter definition:
+ *
+ *		modelElementToViewContainerElement( {
+ *			model: 'heading1',
+ *			view: 'h1',
+ *		}, [ editor.editing.modelToView, editor.data.modelToView ] );
+ *
+ * Or by using a full-flavored view object:
+ *
+ *		modelElementToViewContainerElement( {
+ *			model: 'heading1',
+ *			view: {
+ *				name: 'h1',
+ *				class: [ 'header', 'article-header' ],
+ *				attribute: {
+ *					data-header: 'level-1',
+ *				}
+ *			},
+ *		}, [ editor.editing.modelToView, editor.data.modelToView ] );
+ *
+ * The above will generate the following view element:
+ *
+ *		<h1 class="header article-header" data-header="level-1">...</h1>
+ *
+ * @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition The converter configuration.
+ * @param {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>} dispatchers
+ */
+export function modelElementToViewContainerElement( definition, dispatchers ) {
+	const { model: modelElement, targetView } = normalizeConverterDefinition( definition );
+
+	buildModelConverter()
+		.for( ...dispatchers )
+		.fromElement( modelElement )
+		.toElement( () => createViewElementFromDefinition( targetView, ViewContainerElement ) );
+}
+
+/**
+ * Helper for creating a view element to model element converters.
+ *
+ * Besides converting the view element specified in the `view` property it will also convert all view elements
+ * which match the patterns defined in the `acceptAlso` property. Such a "wide" converters are often needed so the editor
+ * is able to correctly handle a content that was pasted into the editor. Such pasted content may use various
+ * "flavors" of the same editing features (e.g. you may want to handle `<h1>` and `<p class="heading1">` as headings).
+ *
+ * The `model` property defines the model element name to be used by the converter.
+ *
+ * A converter can be defined using a simplified converter definition:
+ *
+ *		viewToModelElement( { model: 'heading1', view: 'h1' }, [ dispatcher ] );
+ *
+ * Or by using a full-flavored definition:
+ *
+ *		viewToModelElement( {
+ *			model: 'heading1',
+ *			view: {
+ *				name: 'p',
+ *				attribute: {
+ *					'data-heading': 'true'
+ *				},
+ *				// You may need to use a high-priority listener to catch elements
+ *				// which are handled by other (usually – more generic) converters too.
+ *				priority: 'high'
+ *			}
+ *		}, [ editor.data.viewToModel ] );
+ *
+ * Or with the `acceptAlso` property to match more patterns:
+ *
+ *		viewToModelElement( {
+ *			model: 'heading1',
+ *			view: 'h1',
+ *			acceptAlso: [
+ *				{ name: 'p', attribute: { 'data-heading': 'level1' }, priority: 'high' },
+ *				{ name: 'h2', class: 'heading-main' },
+ *				{ name: 'div', style: { 'font-weight': 'bold', font-size: '24px' } }
+ *			]
+ *		}, [ editor.data.viewToModel ] );
+ *
+ * The above example will convert an existing view elements:
+ *
+ *		<h1>A heading</h1>
+ *		<h2 class="heading-main">Another heading</h2>
+ *		<p data-heading="level1">Paragraph-like heading</p>
+ *		<div style="font-size:24px; font-weigh:bold;">Another non-semantic header</div>
+ *
+ * into `heading1` model elements so in model it will be represented as:
+ *
+ *		<heading1>A heading</heading1>
+ *		<heading1>Another heading</heading1>
+ *		<heading1>Paragraph-like heading</heading1>
+ *		<heading1>Another non-semantic header</heading1>
+ *
+ * @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition A conversion configuration.
+ * @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
+ */
+export function viewToModelElement( definition, dispatchers ) {
+	const { model: modelElement, sourceViews } = normalizeConverterDefinition( definition );
+
+	const converter = prepareViewConverter( dispatchers, sourceViews );
+
+	converter.toElement( modelElement );
+}
+
+/**
+ * Helper for creating a model attribute to {@link module:engine/view/attributeelement~AttributeElement view attribute element}
+ * converters.
+ *
+ * A converter can be defined by using a simplified converter definition:
+ *
+ *		modelAttributeToViewAttributeElement( 'bold', [
+ *			{
+ *				model: 'true',
+ *				view: 'strong'
+ *			}
+ *		], [ editor.editing.modelToView, editor.data.modelToView ] );
+ *
+ * Or defining full-flavored view objects:
+ *
+ *		modelAttributeToViewAttributeElement( 'fontSize', [
+ *			{
+ *				model: 'big',
+ *				view: {
+ *					name: 'span',
+ *					style: { 'font-size': '1.2em' }
+ *				},
+ *			},
+ *			{
+ *				model: 'small',
+ *				view: {
+ *					name: 'span',
+ *					style: { 'font-size': '0.8em' }
+ *				},
+ *			}
+ *		], [ editor.editing.modelToView, editor.data.modelToView ] );
+ *
+ * The above will generate the following view element from a `fontSize="big"` model attribute:
+ *
+ *		<span style="font-size: 1.2em">...</span>
+ *
+ * @param {String} attributeName The name of the model attribute which should be converted.
+ * @param {Array.<module:engine/conversion/definition-based-converters~ConverterDefinition>} definitions A conversion configuration objects
+ * for each possible attribute value.
+ * @param {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>} dispatchers
+ */
+export function modelAttributeToViewAttributeElement( attributeName, definitions, dispatchers ) {
+	// Create a map of attributeValue - to - ViewElementDefinition.
+	const valueToTargetViewMap = definitions
+		.map( normalizeConverterDefinition )
+		.reduce( ( mapObject, normalizedDefinition ) => {
+			mapObject[ normalizedDefinition.model ] = normalizedDefinition.targetView;
+
+			return mapObject;
+		}, {} );
+
+	buildModelConverter()
+		.for( ...dispatchers )
+		.fromAttribute( attributeName )
+		.toElement( value => {
+			const targetView = valueToTargetViewMap[ value ];
+
+			if ( !targetView ) {
+				return;
+			}
+
+			return createViewElementFromDefinition( targetView, AttributeElement );
+		} );
+}
+
+/**
+ * Helper for creating view element to model attribute converters.
+ *
+ * Besides converting the view element specified in the `view` property it will also convert all view elements
+ * which match the patterns defined in the `acceptAlso` property. Such "wide" converters are often needed so the editor
+ * is able to correctly handle a content that was pasted into the editor. Such pasted content may use various
+ * "flavors" of the same editing features (e.g. bold might be represented as `<b>`, `<strong>` or
+ * `<span style="font-weight:bold">`).
+ *
+ * The `model` property defines the value of the model attribute.
+ *
+ * A converter can be defined using a simplified converter definition:
+ *
+ *		viewToModelAttribute( 'bold', { model: true, view: 'strong' }, [ dispatcher ] );
+ *
+ * Or by using a full-flavored definition:
+ *
+ *		viewToModelAttribute( 'fontSize', {
+ *			model: 'big',
+ *			view: {
+ *				name: 'span',
+ *				style: {
+ *					'font-size': '1.2em'
+ *				}
+ *			}
+ *		}, [ editor.data.viewToModel ] );
+ *
+ * Or with the `acceptAlso` property to match more patterns:
+ *
+ *		viewToModelAttribute( 'fontSize', {
+ *			model: 'big',
+ *			view: {
+ *				name: 'span',
+ *				class: 'text-big'
+ *			},
+ *			acceptAlso: [
+ *				{ name: 'span', attribute: { 'data-size': 'big' } },
+ *				{ name: 'span', class: [ 'font', 'font-huge' ] },
+ *				{ name: 'span', style: { font-size: '18px' } }
+ *			]
+ *		}, [ editor.data.viewToModel ] );
+ *
+ * The above example will convert the following view elements:
+ *
+ *		<p>
+ *			An example text with some big elements:
+ *			<span class="text-big">one</span>,
+ *			<span data-size="big">two</span>,
+ *			<span class="font font-huge">three</span>,
+ *			<span style="font-size: 18px">four</span>
+ *		</p>
+ *
+ * to a `fontSize="big"` model attribute:
+ *
+ *		<paragraph>
+ *			An example text with some big elements:
+ *			<$text fontSize="big">one</$text>,
+ *			<$text fontSize="big">two</$text>,
+ *			<$text fontSize="big">three</$text>,
+ *			<$text fontSize="big">four</$text>
+ *		</paragraph>
+ *
+ * @param {String} attributeName Attribute name to which convert.
+ * @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition A conversion configuration.
+ * @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
+ */
+export function viewToModelAttribute( attributeName, definition, dispatchers ) {
+	const { model: attributeValue, sourceViews } = normalizeConverterDefinition( definition );
+
+	const converter = prepareViewConverter( dispatchers, sourceViews );
+
+	converter.toAttribute( () => ( {
+		key: attributeName,
+		value: attributeValue
+	} ) );
+}
+
+// Normalize a {@link module:engine/conversion/definition-based-converters~ConverterDefinition}
+// into internal object used when building converters.
+//
+// @param {module:engine/conversion/definition-based-converters~ConverterDefinition} definition An object that defines view to model
+// and model to view conversion.
+// @returns {Object}
+function normalizeConverterDefinition( definition ) {
+	const model = definition.model;
+	const view = definition.view;
+
+	// View definition might be defined as a name of an element.
+	const targetView = typeof view == 'string' ? { name: view } : view;
+
+	const sourceViews = Array.from( definition.acceptsAlso ? definition.acceptsAlso : [] );
+
+	// Source views also accepts default view definition used in model-to-view conversion.
+	sourceViews.push( targetView );
+
+	return { model, targetView, sourceViews };
+}
+
+// Helper method for preparing a view converter from passed view definitions.
+//
+// @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
+// @param {Array.<module:engine/view/viewelementdefinition~ViewElementDefinition>} viewDefinitions
+// @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
+function prepareViewConverter( dispatchers, viewDefinitions ) {
+	const converter = buildViewConverter().for( ...dispatchers );
+
+	for ( const viewDefinition of viewDefinitions ) {
+		converter.from( Object.assign( {}, viewDefinition ) );
+
+		if ( viewDefinition.priority ) {
+			converter.withPriority( viewDefinition.priority );
+		}
+	}
+
+	return converter;
+}
+
+// Creates view element instance from provided viewElementDefinition and class.
+//
+// @param {module:engine/view/viewelementdefinition~ViewElementDefinition} viewElementDefinition
+// @param {Function} ViewElementClass
+// @returns {module:engine/view/element~Element}
+function createViewElementFromDefinition( viewElementDefinition, ViewElementClass ) {
+	const element = new ViewElementClass( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+
+	if ( viewElementDefinition.style ) {
+		element.setStyle( viewElementDefinition.style );
+	}
+
+	const classes = viewElementDefinition.class;
+
+	if ( classes ) {
+		element.addClass( ... typeof classes === 'string' ? [ classes ] : classes );
+	}
+
+	return element;
+}
+
+/**
+ * Defines conversion details. It is used in configuration based converters:
+ * - {@link module:engine/conversion/definition-based-converters~modelAttributeToViewAttributeElement}
+ * - {@link module:engine/conversion/definition-based-converters~modelElementToViewContainerElement}
+ * - {@link module:engine/conversion/definition-based-converters~viewToModelAttribute}
+ * - {@link module:engine/conversion/definition-based-converters~viewToModelElement}
+ *
+ * @typedef {Object} ConverterDefinition
+ * @property {String} model Defines to model conversion. When using to element conversion
+ * ({@link module:engine/conversion/definition-based-converters~viewToModelElement}
+ * and {@link module:engine/conversion/definition-based-converters~modelElementToViewContainerElement})
+ * it defines element name. When using to attribute conversion
+ * ({@link module:engine/conversion/definition-based-converters~viewToModelAttribute}
+ * and {@link module:engine/conversion/definition-based-converters~modelAttributeToViewAttributeElement})
+ * it defines attribute value to which it is converted.
+ * @property {module:engine/view/viewelementdefinition~ViewElementDefinition} view Defines model to view conversion and is also used
+ * in view to model conversion pipeline.
+ * @property {Array.<module:engine/view/viewelementdefinition~ViewElementDefinition>} acceptAlso An array with all matched elements that
+ * view to model conversion should also accepts.
+ */

+ 44 - 0
packages/ckeditor5-engine/src/view/viewelementdefinition.jsdoc

@@ -0,0 +1,44 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/view/viewelementdefinition
+ */
+
+/**
+ * An object defining view element used in {@link module:engine/conversion/definition-based-converters} as part of
+ * {@link module:engine/conversion/definition-based-converters~ConverterDefinition}.
+ *
+ * It describe a view element that is used
+ *
+ *		const viewDefinition = {
+ *			name: 'h1',
+ *			class: [ 'foo', 'bar' ]
+ *		};
+ *
+ * Above describes a view element:
+ *
+ *		<h1 class="foo bar">...</h1>
+ *
+ * For elements without attributes you can use shorthand string version:
+ *
+ *		const viewDefinition = 'p';
+ *
+ * which will be treated as:
+ *
+ *		const viewDefinition = {
+ *			name: 'p'
+ *		};
+ *
+ * @typedef {String|Object} module:engine/view/viewelementdefinition~ViewElementDefinition
+ *
+ * @property {String} name View element name.
+ * @property {String|Array.<String>} [class] Class name or array of class names to match. Each name can be
+ * provided in a form of string.
+ * @property {Object} [style] Object with key-value pairs representing styles to match. Each object key
+ * represents style name. Value under that key must be a string.
+ * @property {Object} [attribute] Object with key-value pairs representing attributes to match. Each object key
+ * represents attribute name. Value under that key must be a string.
+ */

+ 479 - 0
packages/ckeditor5-engine/tests/conversion/definition-based-converters.js

@@ -0,0 +1,479 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ModelElement from '../../src/model/element';
+import ModelText from '../../src/model/text';
+import ModelRange from '../../src/model/range';
+
+import ViewElement from '../../src/view/element';
+import ViewAttributeElement from '../../src/view/attributeelement';
+import ViewText from '../../src/view/text';
+
+import { convertText } from '../../src/conversion/view-to-model-converters';
+
+import {
+	modelAttributeToViewAttributeElement,
+	viewToModelAttribute,
+	modelElementToViewContainerElement,
+	viewToModelElement
+} from '../../src/conversion/definition-based-converters';
+
+import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
+import ModelSchema from '../../src/model/schema';
+import ModelWalker from '../../src/model/treewalker';
+import ModelTextProxy from '../../src/model/textproxy';
+import Model from '../../src/model/model';
+import ModelPosition from '../../src/model/position';
+import EditingController from '../../src/controller/editingcontroller';
+
+function viewAttributesToString( item ) {
+	let result = '';
+
+	for ( const key of item.getAttributeKeys() ) {
+		const value = item.getAttribute( key );
+
+		if ( value ) {
+			result += ' ' + key + '="' + value + '"';
+		}
+	}
+
+	return result;
+}
+
+function modelToString( item ) {
+	let result = '';
+
+	if ( item instanceof ModelTextProxy ) {
+		const attributes = modelAttributesToString( item );
+
+		result = attributes ? '<$text' + attributes + '>' + item.data + '</$text>' : item.data;
+	} else {
+		const walker = new ModelWalker( { boundaries: ModelRange.createIn( item ), shallow: true } );
+
+		for ( const value of walker ) {
+			result += modelToString( value.item );
+		}
+
+		if ( item instanceof ModelElement ) {
+			const attributes = modelAttributesToString( item );
+
+			result = '<' + item.name + attributes + '>' + result + '</' + item.name + '>';
+		}
+	}
+
+	return result;
+}
+
+function modelAttributesToString( item ) {
+	let result = '';
+
+	for ( const attr of item.getAttributes() ) {
+		result += ' ' + attr[ 0 ] + '="' + attr[ 1 ] + '"';
+	}
+
+	return result;
+}
+
+function viewToString( item ) {
+	let result = '';
+
+	if ( item instanceof ViewText ) {
+		result = item.data;
+	} else {
+		// ViewElement or ViewDocumentFragment.
+		for ( const child of item.getChildren() ) {
+			result += viewToString( child );
+		}
+
+		if ( item instanceof ViewElement ) {
+			result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
+		}
+	}
+
+	return result;
+}
+
+describe( 'definition-based-converters', () => {
+	let model, dispatcher, modelDoc, modelRoot, viewRoot, controller, additionalData, schema;
+
+	beforeEach( () => {
+		model = new Model();
+	} );
+
+	function setupViewToModelTests() {
+		additionalData = { context: [ '$root' ] };
+		schema = new ModelSchema();
+		dispatcher = new ViewConversionDispatcher( model, { schema } );
+	}
+
+	function setupModelToViewTests() {
+		modelDoc = model.document;
+		modelRoot = modelDoc.createRoot();
+
+		controller = new EditingController( model );
+		controller.createRoot( 'div' );
+
+		viewRoot = controller.view.getRoot();
+		dispatcher = controller.modelToView;
+	}
+
+	describe( 'Attribute converters', () => {
+		function testModelConversion( definition, expectedConversion ) {
+			modelAttributeToViewAttributeElement( 'foo', [ definition ], [ dispatcher ] );
+
+			const modelElement = new ModelText( 'foo', { foo: 'bar' } );
+
+			model.change( writer => {
+				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
+			} );
+
+			expect( viewToString( viewRoot ) ).to.equal( expectedConversion );
+
+			model.change( writer => {
+				writer.removeAttribute( 'foo', modelElement );
+			} );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
+		}
+
+		describe( 'model to view conversion', () => {
+			beforeEach( () => {
+				setupModelToViewTests();
+			} );
+
+			it( 'using passed view element name', () => {
+				testModelConversion( { model: 'bar', view: 'strong' }, '<div><strong>foo</strong></div>' );
+			} );
+
+			it( 'using passed view element object', () => {
+				testModelConversion( { model: 'bar', view: { name: 'strong' } }, '<div><strong>foo</strong></div>' );
+			} );
+
+			it( 'using passed view element object with style object', () => {
+				testModelConversion( {
+					model: 'bar',
+					view: { name: 'span', style: { 'font-weight': 'bold' } }
+				}, '<div><span style="font-weight:bold;">foo</span></div>' );
+			} );
+
+			it( 'using passed view element object with class string', () => {
+				testModelConversion( { model: 'bar', view: { name: 'span', class: 'foo' } }, '<div><span class="foo">foo</span></div>' );
+			} );
+
+			it( 'using passed view element object with class array', () => {
+				testModelConversion( {
+					model: 'bar',
+					view: { name: 'span', class: [ 'foo', 'foo-bar' ] }
+				}, '<div><span class="foo foo-bar">foo</span></div>' );
+			} );
+
+			it( 'using passed view element object with attributes', () => {
+				testModelConversion( {
+					model: 'bar',
+					view: { name: 'span', attribute: { 'data-foo': 'bar' } }
+				}, '<div><span data-foo="bar">foo</span></div>' );
+			} );
+
+			it( 'should convert when changing attribute', () => {
+				const definition1 = { model: 'bar', view: { name: 'span', class: 'bar' } };
+				const definition2 = { model: 'baz', view: { name: 'span', class: 'baz' } };
+
+				modelAttributeToViewAttributeElement( 'foo', [ definition1, definition2 ], [ dispatcher ] );
+
+				const modelElement = new ModelText( 'foo', { foo: 'bar' } );
+
+				model.change( writer => {
+					writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
+				} );
+
+				expect( viewToString( viewRoot ) ).to.equal( '<div><span class="bar">foo</span></div>' );
+
+				model.change( writer => {
+					writer.setAttribute( 'foo', 'baz', modelElement );
+				} );
+
+				expect( viewToString( viewRoot ) ).to.equal( '<div><span class="baz">foo</span></div>' );
+			} );
+
+			it( 'should do nothing for undefined value', () => {
+				modelAttributeToViewAttributeElement( 'foo', [ { model: 'bar', view: 'strong' } ], [ dispatcher ] );
+
+				const modelElement = new ModelText( 'foo', { foo: 'baz' } );
+
+				model.change( writer => {
+					writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
+				} );
+
+				expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
+			} );
+		} );
+
+		describe( 'view to model conversion', () => {
+			beforeEach( () => {
+				setupViewToModelTests();
+
+				schema.registerItem( 'div', '$block' );
+
+				schema.allow( { name: '$inline', attributes: [ 'foo' ], inside: '$root' } );
+				schema.allow( { name: '$text', inside: '$root' } );
+
+				dispatcher.on( 'text', convertText() );
+			} );
+
+			it( 'should convert using element name', () => {
+				viewToModelAttribute( 'foo', { model: 'bar', view: 'strong' }, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
+			} );
+
+			it( 'should convert using object', () => {
+				viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'strong' } }, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
+			} );
+
+			it( 'should convert using class string', () => {
+				viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'span', class: 'foo' } }, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewAttributeElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
+			} );
+
+			it( 'should convert using class array', () => {
+				viewToModelAttribute( 'foo', {
+					model: 'bar',
+					view: { name: 'span', class: [ 'foo', 'bar' ] }
+				}, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewAttributeElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
+			} );
+
+			it( 'should convert using style object', () => {
+				viewToModelAttribute( 'foo', {
+					model: 'bar',
+					view: { name: 'span', style: { 'font-weight': 'bold' } }
+				}, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewAttributeElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
+			} );
+
+			it( 'should convert using attributes object', () => {
+				viewToModelAttribute( 'foo', {
+					model: 'bar',
+					view: { name: 'span', attribute: { 'data-foo': 'bar' } }
+				}, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
+			} );
+
+			it( 'should convert using acceptAlso array', () => {
+				viewToModelAttribute( 'foo', {
+					model: 'bar',
+					view: 'strong',
+					acceptsAlso: [
+						{ name: 'span', class: [ 'foo', 'bar' ] },
+						{ name: 'span', attribute: { 'data-foo': 'bar' } }
+					]
+				}, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
+			} );
+
+			it( 'should convert using priority', () => {
+				viewToModelAttribute( 'foo', { model: 'baz', view: 'strong' }, [ dispatcher ] );
+				viewToModelAttribute( 'foo', { model: 'bar', view: { name: 'strong', priority: 'high' } }, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
+			} );
+		} );
+	} );
+
+	describe( 'Element converters', () => {
+		function testModelConversion( definition, expectedResult ) {
+			modelElementToViewContainerElement( definition, [ dispatcher ] );
+
+			const modelElement = new ModelElement( 'foo', null, new ModelText( 'bar' ) );
+
+			model.change( writer => {
+				writer.insert( modelElement, ModelPosition.createAt( modelRoot, 0 ) );
+			} );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div>' + expectedResult + '</div>' );
+		}
+
+		describe( 'model to view conversion', () => {
+			beforeEach( () => {
+				setupModelToViewTests();
+			} );
+
+			it( 'using passed view element name', () => {
+				testModelConversion( { model: 'foo', view: 'code' }, '<code>bar</code>' );
+			} );
+
+			it( 'using passed view element object', () => {
+				testModelConversion( { model: 'foo', view: { name: 'code' } }, '<code>bar</code>' );
+			} );
+
+			it( 'using passed view element object with style object', () => {
+				testModelConversion( {
+					model: 'foo',
+					view: { name: 'span', style: { 'font-weight': 'bold' } }
+				}, '<span style="font-weight:bold;">bar</span>' );
+			} );
+
+			it( 'using passed view element object with class string', () => {
+				testModelConversion( { model: 'foo', view: { name: 'span', class: 'foo' } }, '<span class="foo">bar</span>' );
+			} );
+
+			it( 'using passed view element object with class array', () => {
+				testModelConversion( {
+					model: 'foo',
+					view: { name: 'span', class: [ 'foo', 'foo-bar' ] }
+				}, '<span class="foo foo-bar">bar</span>' );
+			} );
+
+			it( 'using passed view element object with attributes', () => {
+				testModelConversion( {
+					model: 'foo',
+					view: { name: 'span', attribute: { 'data-foo': 'bar' } }
+				}, '<span data-foo="bar">bar</span>' );
+			} );
+		} );
+
+		describe( 'view to model conversion', () => {
+			beforeEach( () => {
+				setupViewToModelTests();
+
+				schema.registerItem( 'div', '$block' );
+				schema.registerItem( 'bar', '$block' );
+				schema.registerItem( 'baz', '$block' );
+
+				schema.allow( { name: '$inline', attribute: [ 'foo' ], inside: '$root' } );
+				schema.allow( { name: '$text', inside: '$inline' } );
+
+				dispatcher.on( 'text', convertText() );
+			} );
+
+			it( 'should convert using element name', () => {
+				viewToModelElement( { model: 'bar', view: 'strong' }, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
+			} );
+
+			it( 'should convert using object', () => {
+				viewToModelElement( { model: 'bar', view: { name: 'strong' } }, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
+			} );
+
+			it( 'should convert using class string', () => {
+				viewToModelElement( { model: 'bar', view: { name: 'span', class: 'foo' } }, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
+			} );
+
+			it( 'should convert using class array', () => {
+				viewToModelElement( { model: 'bar', view: { name: 'span', class: [ 'foo', 'bar' ] } }, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
+			} );
+
+			it( 'should convert using style object', () => {
+				viewToModelElement( { model: 'bar', view: { name: 'span', style: { 'font-weight': 'bold' } } }, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
+			} );
+
+			it( 'should convert using attributes object', () => {
+				viewToModelElement( { model: 'bar', view: { name: 'span', attribute: { 'data-foo': 'bar' } } }, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
+			} );
+
+			it( 'should convert using acceptAlso array', () => {
+				viewToModelElement( {
+					model: 'bar',
+					view: 'strong',
+					acceptsAlso: [
+						{ name: 'span', class: [ 'foo', 'bar' ] },
+						{ name: 'span', attribute: { 'data-foo': 'bar' } }
+					]
+				}, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
+			} );
+
+			it( 'should convert using priority', () => {
+				viewToModelElement( { model: 'baz', view: 'strong' }, [ dispatcher ] );
+				viewToModelElement( { model: 'bar', view: { name: 'strong', priority: 'high' } }, [ dispatcher ] );
+
+				const conversionResult = dispatcher.convert(
+					new ViewElement( 'strong', null, new ViewText( 'foo' ) ), additionalData
+				);
+
+				expect( modelToString( conversionResult ) ).to.equal( '<bar>foo</bar>' );
+			} );
+		} );
+	} );
+} );