浏览代码

Merge pull request #554 from ckeditor/t/306

Refactored model utils.
Piotr Jasiun 9 年之前
父节点
当前提交
9f8338bb5c
共有 26 个文件被更改,包括 1329 次插入1069 次删除
  1. 1 1
      packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js
  2. 14 11
      packages/ckeditor5-engine/src/conversion/model-to-view-converters.js
  3. 1 1
      packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js
  4. 1 1
      packages/ckeditor5-engine/src/datacontroller.js
  5. 126 0
      packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js
  6. 103 121
      packages/ckeditor5-engine/tests/_utils-tests/model.js
  7. 17 4
      packages/ckeditor5-engine/tests/_utils-tests/view.js
  8. 212 340
      packages/ckeditor5-engine/tests/_utils/model.js
  9. 77 55
      packages/ckeditor5-engine/tests/_utils/view.js
  10. 34 30
      packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js
  11. 8 7
      packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js
  12. 6 3
      packages/ckeditor5-engine/tests/datacontroller.js
  13. 103 0
      packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js
  14. 4 3
      packages/ckeditor5-engine/tests/editingcontroller.js
  15. 7 6
      packages/ckeditor5-engine/tests/model/composer/composer.js
  16. 96 71
      packages/ckeditor5-engine/tests/model/composer/deletecontents.js
  17. 212 161
      packages/ckeditor5-engine/tests/model/composer/modifyselection.js
  18. 9 9
      packages/ckeditor5-engine/tests/model/writer.js
  19. 31 19
      packages/ckeditor5-engine/tests/view/writer/breakAt.js
  20. 21 21
      packages/ckeditor5-engine/tests/view/writer/insert.js
  21. 25 13
      packages/ckeditor5-engine/tests/view/writer/mergeAt.js
  22. 20 12
      packages/ckeditor5-engine/tests/view/writer/move.js
  23. 15 11
      packages/ckeditor5-engine/tests/view/writer/remove.js
  24. 84 78
      packages/ckeditor5-engine/tests/view/writer/unwrap.js
  25. 73 63
      packages/ckeditor5-engine/tests/view/writer/wrap.js
  26. 29 28
      packages/ckeditor5-engine/tests/view/writer/wrapposition.js

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

@@ -161,7 +161,7 @@ export function convertSelectionAttribute( elementCreator ) {
 
 
 		const viewElement = elementCreator instanceof ViewElement ?
 		const viewElement = elementCreator instanceof ViewElement ?
 				elementCreator.clone( true ) :
 				elementCreator.clone( true ) :
-				elementCreator( data.value, selection, consumable, conversionApi );
+				elementCreator( data.value, data, selection, consumable, conversionApi );
 
 
 		viewPosition = viewWriter.wrapPosition( viewPosition, viewElement );
 		viewPosition = viewWriter.wrapPosition( viewPosition, viewElement );
 
 

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

@@ -195,6 +195,7 @@ export function removeAttribute( attributeCreator ) {
  * the copy will become the wrapping element. If `Function` is provided, it is passed all the parameters of the
  * the copy will become the wrapping element. If `Function` is provided, it is passed all the parameters of the
  * {@link engine.conversion.ModelConversionDispatcher#event:setAttribute setAttribute event}. It's expected that the
  * {@link engine.conversion.ModelConversionDispatcher#event:setAttribute setAttribute event}. It's expected that the
  * function returns a {@link engine.view.Element}. The result of the function will be the wrapping element.
  * function returns a {@link engine.view.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
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * {@link engine.conversion.ModelConversionDispatcher}).
  * {@link engine.conversion.ModelConversionDispatcher}).
@@ -209,25 +210,27 @@ export function removeAttribute( attributeCreator ) {
  */
  */
 export function wrap( elementCreator ) {
 export function wrap( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
 	return ( evt, data, consumable, conversionApi ) => {
-		consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
-
 		let viewRange = conversionApi.mapper.toViewRange( data.range );
 		let viewRange = conversionApi.mapper.toViewRange( data.range );
 
 
 		const viewElement = ( elementCreator instanceof ViewElement ) ?
 		const viewElement = ( elementCreator instanceof ViewElement ) ?
 			elementCreator.clone( true ) :
 			elementCreator.clone( true ) :
 			elementCreator( data.attributeNewValue, data, consumable, conversionApi );
 			elementCreator( data.attributeNewValue, data, consumable, conversionApi );
 
 
-		// If this is a change event (because old value is not empty) and the creator is a function (so
-		// it may create different view elements basing on attribute value) we have to create
-		// view element basing on old value and unwrap it before wrapping with a newly created view element.
-		if ( data.attributeOldValue !== null && !( elementCreator instanceof ViewElement ) ) {
-			const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi );
-			viewRange = viewWriter.unwrap( viewRange, oldViewElement, evt.priority );
-		}
+		if ( viewElement ) {
+			consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
 
 
-		viewWriter.wrap( viewRange, viewElement, evt.priority );
+			// If this is a change event (because old value is not empty) and the creator is a function (so
+			// it may create different view elements basing on attribute value) we have to create
+			// view element basing on old value and unwrap it before wrapping with a newly created view element.
+			if ( data.attributeOldValue !== null && !( elementCreator instanceof ViewElement ) ) {
+				const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi );
+				viewRange = viewWriter.unwrap( viewRange, oldViewElement, evt.priority );
+			}
 
 
-		evt.stop();
+			viewWriter.wrap( viewRange, viewElement );
+
+			evt.stop();
+		}
 	};
 	};
 }
 }
 
 

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

@@ -57,7 +57,7 @@ import extend from '../../utils/lib/lodash/extend.js';
  *			const viewElement = new ViewElement( 'p' );
  *			const viewElement = new ViewElement( 'p' );
  *
  *
  *			// Bind the newly created view element to model element so positions will map accordingly in future.
  *			// Bind the newly created view element to model element so positions will map accordingly in future.
- *			conversionApi.mapper.bindElements( data.item, viewNode );
+ *			conversionApi.mapper.bindElements( data.item, viewElement );
  *
  *
  *			// Add the newly created view element to the view.
  *			// Add the newly created view element to the view.
  *			viewWriter.insert( viewPosition, viewElement );
  *			viewWriter.insert( viewPosition, viewElement );

+ 1 - 1
packages/ckeditor5-engine/src/datacontroller.js

@@ -24,7 +24,7 @@ import ModelPosition from './model/position.js';
  *
  *
  * * {@link engine.dataProcessor.DataProcessor data processor},
  * * {@link engine.dataProcessor.DataProcessor data processor},
  * * {@link engine.conversion.ModelConversionDispatcher model to view} and
  * * {@link engine.conversion.ModelConversionDispatcher model to view} and
- * {@link engine.conversion.ViewConversionDispatcher view to model} converters.
+ * * {@link engine.conversion.ViewConversionDispatcher view to model} converters.
  *
  *
  * @memberOf engine
  * @memberOf engine
  */
  */

+ 126 - 0
packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js

@@ -0,0 +1,126 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import BasicHtmlWriter from './basichtmlwriter.js';
+import DomConverter from '../view/domconverter.js';
+import { NBSP_FILLER } from '../view/filler.js';
+
+/**
+ * XmlDataProcessor class.
+ * This data processor implementation uses XML as input/output data.
+ * This class is needed because unlike HTML, XML allows to use any tag with any value.
+ * E.g. `<link>Text</link>` is a valid XML but invalid HTML.
+ *
+ * @memberOf engine.dataProcessor
+ * @implements engine.dataProcessor.DataProcessor
+ */
+export default class XmlDataProcessor {
+	/**
+	 * Creates a new instance of the XmlDataProcessor class.
+	 *
+	 * @param {Object} options Configuration options.
+	 * @param {Array<String>} [options.namespaces=[]] List of namespaces allowed to use in XML input.
+	 */
+	constructor( options = {} ) {
+		/**
+		 * List of namespaces allowed to use in XML input.
+		 *
+		 * E.g. Registering namespaces [ 'attribute', 'container' ] allows to use `<attirbute:tagName></attribute:tagName>` and
+		 * `<container:tagName></container:tagName>` input. It is mainly for debugging.
+		 *
+		 * @public
+		 * @member {DOMParser} engine.dataProcessor.XmlDataProcessor#namespaces
+		 */
+		this.namespaces = options.namespaces || [];
+
+		/**
+		 * DOMParser instance used to parse XML string to XMLDocument.
+		 *
+		 * @private
+		 * @member {DOMParser} engine.dataProcessor.XmlDataProcessor#_domParser
+		 */
+		this._domParser = new DOMParser();
+
+		/**
+		 * DOM converter used to convert DOM elements to view elements.
+		 *
+		 * @private
+		 * @member {engine.view.DomConverter} engine.dataProcessor.XmlDataProcessor#_domConverter.
+		 */
+		this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
+
+		/**
+		 * BasicHtmlWriter instance used to convert DOM elements to XML string.
+		 * There is no need to use dedicated for XML writer because BasicHtmlWriter works well in this case.
+		 *
+		 * @private
+		 * @member {engine.dataProcessor.BasicHtmlWriter} engine.dataProcessor.HtmlDataProcessor#_htmlWriter
+		 */
+		this._htmlWriter = new BasicHtmlWriter();
+	}
+
+	/**
+	 * Converts provided {@link engine.view.DocumentFragment DocumentFragment} to data format - in this case XML string.
+	 *
+	 * @param {engine.view.DocumentFragment} viewFragment
+	 * @returns {String} XML string.
+	 */
+	toData( viewFragment ) {
+		// Convert view DocumentFragment to DOM DocumentFragment.
+		const domFragment = this._domConverter.viewToDom( viewFragment, document );
+
+		// Convert DOM DocumentFragment to XML output.
+		// There is no need to use dedicated for XML serializing method because BasicHtmlWriter works well in this case.
+		return this._htmlWriter.getHtml( domFragment );
+	}
+
+	/**
+	 * Converts provided XML string to view tree.
+	 *
+	 * @param {String} data XML string.
+	 * @returns {engine.view.Node|engine.view.DocumentFragment|null} Converted view element.
+	 */
+	toView( data ) {
+		// Convert input XML data to DOM DocumentFragment.
+		const domFragment = this._toDom( data );
+
+		// Convert DOM DocumentFragment to view DocumentFragment.
+		return this._domConverter.domToView( domFragment );
+	}
+
+	/**
+	 * Converts XML String to its DOM representation. Returns DocumentFragment, containing nodes parsed from
+	 * provided data.
+	 *
+	 * @private
+	 * @param {String} data
+	 * @returns {DocumentFragment}
+	 */
+	_toDom( data ) {
+		// Stringify namespaces.
+		const namespaces = this.namespaces.map( nsp => `xmlns:${ nsp }="nsp"` ).join( ' ' );
+
+		// Wrap data into root element with optional namespace definitions.
+		data = `<xml ${ namespaces }>${ data }</xml>`;
+
+		const document = this._domParser.parseFromString( data, 'text/xml' );
+
+		// Parse validation.
+		const parserError = document.querySelector( 'parsererror' );
+
+		if ( parserError ) {
+			throw new Error( 'Parse error - ' + parserError.querySelector( 'div' ).textContent );
+		}
+
+		const fragment = document.createDocumentFragment();
+		const nodes = document.documentElement.childNodes;
+
+		while ( nodes.length > 0 ) {
+			fragment.appendChild( nodes[ 0 ] );
+		}
+
+		return fragment;
+	}
+}

+ 103 - 121
packages/ckeditor5-engine/tests/_utils-tests/model.js

@@ -10,6 +10,7 @@ import Element from '/ckeditor5/engine/model/element.js';
 import Text from '/ckeditor5/engine/model/text.js';
 import Text from '/ckeditor5/engine/model/text.js';
 import Range from '/ckeditor5/engine/model/range.js';
 import Range from '/ckeditor5/engine/model/range.js';
 import Position from '/ckeditor5/engine/model/position.js';
 import Position from '/ckeditor5/engine/model/position.js';
+import count from '/ckeditor5/utils/count.js';
 
 
 describe( 'model test utils', () => {
 describe( 'model test utils', () => {
 	let document, root, selection, sandbox;
 	let document, root, selection, sandbox;
@@ -20,6 +21,15 @@ describe( 'model test utils', () => {
 		selection = document.selection;
 		selection = document.selection;
 		sandbox = sinon.sandbox.create();
 		sandbox = sinon.sandbox.create();
 		selection.removeAllRanges();
 		selection.removeAllRanges();
+
+		document.schema.registerItem( 'a', '$inline' );
+		document.schema.allow( { name: 'a', inside: '$root' } );
+		document.schema.registerItem( 'b', '$inline' );
+		document.schema.allow( { name: 'b', inside: '$root' } );
+		document.schema.registerItem( 'c', '$inline' );
+		document.schema.allow( { name: 'c', inside: '$root' } );
+		document.schema.registerItem( 'paragraph', '$block' );
+		document.schema.allow( { name: '$text', inside: '$root' } );
 	} );
 	} );
 
 
 	afterEach( () => {
 	afterEach( () => {
@@ -41,7 +51,7 @@ describe( 'model test utils', () => {
 			root.appendChildren( new Element( 'b', null, new Text( 'btext' ) ) );
 			root.appendChildren( new Element( 'b', null, new Text( 'btext' ) ) );
 			document.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
 			document.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
 
 
-			expect( getData( document ) ).to.equal( '<selection><b>btext</b></selection>' );
+			expect( getData( document ) ).to.equal( '[<b>btext</b>]' );
 			sinon.assert.calledOnce( stringifySpy );
 			sinon.assert.calledOnce( stringifySpy );
 			sinon.assert.calledWithExactly( stringifySpy, root, document.selection );
 			sinon.assert.calledWithExactly( stringifySpy, root, document.selection );
 		} );
 		} );
@@ -50,7 +60,7 @@ describe( 'model test utils', () => {
 			root.appendChildren( 'நிலைக்கு' );
 			root.appendChildren( 'நிலைக்கு' );
 			document.selection.addRange( Range.createFromParentsAndOffsets( root, 2, root, 6 ) );
 			document.selection.addRange( Range.createFromParentsAndOffsets( root, 2, root, 6 ) );
 
 
-			expect( getData( document ) ).to.equal( 'நி<selection>லைக்</selection>கு' );
+			expect( getData( document ) ).to.equal( 'நி[லைக்]கு' );
 		} );
 		} );
 
 
 		it( 'should throw an error when passing invalid document', () => {
 		it( 'should throw an error when passing invalid document', () => {
@@ -65,7 +75,6 @@ describe( 'model test utils', () => {
 			const parseSpy = sandbox.spy( setData, '_parse' );
 			const parseSpy = sandbox.spy( setData, '_parse' );
 			const options = {};
 			const options = {};
 			const data = '<b>btext</b>text';
 			const data = '<b>btext</b>text';
-			document.schema.registerItem( 'b', '$inline' );
 
 
 			setData( document, data, options );
 			setData( document, data, options );
 
 
@@ -78,8 +87,7 @@ describe( 'model test utils', () => {
 		it( 'should use parse method with selection', () => {
 		it( 'should use parse method with selection', () => {
 			const parseSpy = sandbox.spy( setData, '_parse' );
 			const parseSpy = sandbox.spy( setData, '_parse' );
 			const options = {};
 			const options = {};
-			const data = '<selection><b>btext</b></selection>';
-			document.schema.registerItem( 'b', '$inline' );
+			const data = '[<b>btext</b>]';
 
 
 			setData( document, data, options );
 			setData( document, data, options );
 
 
@@ -90,52 +98,50 @@ describe( 'model test utils', () => {
 		} );
 		} );
 
 
 		it( 'should insert text', () => {
 		it( 'should insert text', () => {
-			test( 'this is test text', '<selection />this is test text' );
+			test( 'this is test text', '[]this is test text' );
 		} );
 		} );
 
 
 		it( 'should insert text with selection around', () => {
 		it( 'should insert text with selection around', () => {
-			test( '<selection>this is test text</selection>' );
+			test( '[this is test text]' );
 		} );
 		} );
 
 
 		it( 'should insert text with selection inside #1', () => {
 		it( 'should insert text with selection inside #1', () => {
-			test( 'this <selection>is test</selection> text' );
+			test( 'this [is test] text' );
 		} );
 		} );
 
 
 		it( 'should insert text with selection inside #2', () => {
 		it( 'should insert text with selection inside #2', () => {
-			test( '<selection>this is test</selection> text' );
+			test( '[this is test] text' );
 		} );
 		} );
 
 
 		it( 'should insert text with selection inside #2', () => {
 		it( 'should insert text with selection inside #2', () => {
-			test( 'this is <selection>test text</selection>' );
+			test( 'this is [test text]' );
 		} );
 		} );
 
 
 		it( 'should insert element', () => {
 		it( 'should insert element', () => {
-			document.schema.registerItem( 'b', '$inline' );
-			test( '<b>foo bar</b>', '<selection /><b>foo bar</b>' );
+			test( '<b>foo bar</b>', '[]<b>foo bar</b>' );
 		} );
 		} );
 
 
 		it( 'should insert element with selection inside #1', () => {
 		it( 'should insert element with selection inside #1', () => {
-			document.schema.registerItem( 'b', '$inline' );
-			test( '<b><selection>foo </selection>bar</b>' );
+			test( '<b>[foo ]bar</b>' );
 		} );
 		} );
 
 
 		it( 'should insert element with selection inside #2', () => {
 		it( 'should insert element with selection inside #2', () => {
-			document.schema.registerItem( 'b', '$inline' );
-			test( '<selection><b>foo </selection>bar</b>' );
+			test( '[<b>foo ]bar</b>' );
 		} );
 		} );
 
 
 		it( 'should insert element with selection inside #3', () => {
 		it( 'should insert element with selection inside #3', () => {
-			document.schema.registerItem( 'b', '$inline' );
-			test( '<b><selection>foo bar</b></selection>' );
+			test( '<b>[foo bar</b>]' );
 		} );
 		} );
 
 
 		it( 'should insert backward selection', () => {
 		it( 'should insert backward selection', () => {
-			document.schema.registerItem( 'b', '$inline' );
-			test( '<b><selection backward>foo bar</b></selection>' );
+			setData( document, '<b>[foo bar</b>]', { lastRangeBackward: true } );
+
+			expect( getData( document ) ).to.equal( '<b>[foo bar</b>]' );
+			expect( document.selection.isBackward ).to.true;
 		} );
 		} );
 
 
 		it( 'should support unicode', () => {
 		it( 'should support unicode', () => {
-			test( 'நி<selection>லைக்</selection>கு' );
+			test( 'நி[லைக்]கு' );
 		} );
 		} );
 
 
 		it( 'should throw an error when passing invalid document', () => {
 		it( 'should throw an error when passing invalid document', () => {
@@ -155,7 +161,7 @@ describe( 'model test utils', () => {
 	describe( 'stringify', () => {
 	describe( 'stringify', () => {
 		it( 'should stringify text', () => {
 		it( 'should stringify text', () => {
 			const text = new Text( 'text', { underline: true, bold: true } );
 			const text = new Text( 'text', { underline: true, bold: true } );
-			expect( stringify( text ) ).to.equal( '<$text bold=true underline=true>text</$text>' );
+			expect( stringify( text ) ).to.equal( '<$text bold="true" underline="true">text</$text>' );
 		} );
 		} );
 
 
 		it( 'should stringify element', () => {
 		it( 'should stringify element', () => {
@@ -195,14 +201,14 @@ describe( 'model test utils', () => {
 		it( 'writes element attributes', () => {
 		it( 'writes element attributes', () => {
 			root.appendChildren(
 			root.appendChildren(
 				new Element( 'a', { foo: true, bar: 1, car: false }, [
 				new Element( 'a', { foo: true, bar: 1, car: false }, [
-					new Element( 'b', { fooBar: 'x y', barFoo: { x: 1, y: 2 } } )
+					new Element( 'b', { fooBar: 'x y', barFoo: JSON.stringify( { x: 1, y: 2 } ) } )
 				] )
 				] )
 			);
 			);
 
 
 			// Note: attributes are written in a very simplistic way, because they are not to be parsed. They are just
 			// Note: attributes are written in a very simplistic way, because they are not to be parsed. They are just
 			// to be compared in the tests with some patterns.
 			// to be compared in the tests with some patterns.
 			expect( stringify( root ) ).to.equal(
 			expect( stringify( root ) ).to.equal(
-				'<a bar=1 car=false foo=true><b barFoo={"x":1,"y":2} fooBar="x y"></b></a>'
+				'<a bar="1" car="false" foo="true"><b barFoo="{"x":1,"y":2}" fooBar="x y"></b></a>'
 			);
 			);
 		} );
 		} );
 
 
@@ -217,10 +223,9 @@ describe( 'model test utils', () => {
 			] );
 			] );
 
 
 			expect( stringify( root ) ).to.equal(
 			expect( stringify( root ) ).to.equal(
-				'<$text bold=true>foo</$text>' +
-				'bar' +
-				'<$text bold=true italic=true>bom</$text>' +
-				'<a><$text bold=true underline=true>pom</$text></a>'
+				// Because of https://github.com/ckeditor/ckeditor5-engine/issues/562 attributes are not merged
+				'<$text bold="true">foo</$text>bar<$text italic="true"><$text bold="true">bom</$text></$text>' +
+				'<a><$text bold="true" underline="true">pom</$text></a>'
 			);
 			);
 		} );
 		} );
 
 
@@ -244,7 +249,7 @@ describe( 'model test utils', () => {
 				selection.collapse( root );
 				selection.collapse( root );
 
 
 				expect( stringify( root, selection ) ).to.equal(
 				expect( stringify( root, selection ) ).to.equal(
-					'<selection />'
+					'[]'
 				);
 				);
 			} );
 			} );
 
 
@@ -256,7 +261,7 @@ describe( 'model test utils', () => {
 				selection.collapse( root );
 				selection.collapse( root );
 
 
 				expect( stringify( root, selection ) ).to.equal(
 				expect( stringify( root, selection ) ).to.equal(
-					'<selection /><a></a>foo<$text bold=true>bar</$text><b></b>'
+					'[]<a></a>foo<$text bold="true">bar</$text><b></b>'
 				);
 				);
 			} );
 			} );
 
 
@@ -264,7 +269,7 @@ describe( 'model test utils', () => {
 				selection.collapse( root, 3 );
 				selection.collapse( root, 3 );
 
 
 				expect( stringify( root, selection ) ).to.equal(
 				expect( stringify( root, selection ) ).to.equal(
-					'<a></a>fo<selection />o<$text bold=true>bar</$text><b></b>'
+					'<a></a>fo[]o<$text bold="true">bar</$text><b></b>'
 				);
 				);
 			} );
 			} );
 
 
@@ -272,7 +277,7 @@ describe( 'model test utils', () => {
 				selection.collapse( elA, 'after' );
 				selection.collapse( elA, 'after' );
 
 
 				expect( stringify( root, selection ) ).to.equal(
 				expect( stringify( root, selection ) ).to.equal(
-					'<a></a><selection />foo<$text bold=true>bar</$text><b></b>'
+					'<a></a>[]foo<$text bold="true">bar</$text><b></b>'
 				);
 				);
 			} );
 			} );
 
 
@@ -280,8 +285,10 @@ describe( 'model test utils', () => {
 				selection.collapse( elB, 'before' );
 				selection.collapse( elB, 'before' );
 
 
 				expect( stringify( root, selection ) ).to.equal(
 				expect( stringify( root, selection ) ).to.equal(
-					'<a></a>foo<$text bold=true>bar</$text><selection bold=true /><b></b>'
+					'<a></a>foo<$text bold="true">bar[]</$text><b></b>'
 				);
 				);
+				expect( selection.getAttribute( 'bold' ) ).to.true;
+				expect( count( selection.getAttributes() ) ).to.equal( 1 );
 			} );
 			} );
 
 
 			it( 'writes selection collapsed at the end of the root', () => {
 			it( 'writes selection collapsed at the end of the root', () => {
@@ -291,16 +298,7 @@ describe( 'model test utils', () => {
 				selection.clearAttributes();
 				selection.clearAttributes();
 
 
 				expect( stringify( root, selection ) ).to.equal(
 				expect( stringify( root, selection ) ).to.equal(
-					'<a></a>foo<$text bold=true>bar</$text><b></b><selection />'
-				);
-			} );
-
-			it( 'writes selection attributes', () => {
-				selection.collapse( root );
-				selection.setAttributesTo( { italic: true, bold: true } );
-
-				expect( stringify( root, selection )  ).to.equal(
-					'<selection bold=true italic=true /><a></a>foo<$text bold=true>bar</$text><b></b>'
+					'<a></a>foo<$text bold="true">bar</$text><b></b>[]'
 				);
 				);
 			} );
 			} );
 
 
@@ -308,8 +306,10 @@ describe( 'model test utils', () => {
 				selection.collapse( root, 5 );
 				selection.collapse( root, 5 );
 
 
 				expect( stringify( root, selection ) ).to.equal(
 				expect( stringify( root, selection ) ).to.equal(
-					'<a></a>foo<$text bold=true>b<selection bold=true />ar</$text><b></b>'
+					'<a></a>foo<$text bold="true">b[]ar</$text><b></b>'
 				);
 				);
+				expect( selection.getAttribute( 'bold' ) ).to.true;
+				expect( count( selection.getAttributes() ) ).to.equal( 1 );
 			} );
 			} );
 
 
 			it( 'writes flat selection containing couple of nodes', () => {
 			it( 'writes flat selection containing couple of nodes', () => {
@@ -318,7 +318,7 @@ describe( 'model test utils', () => {
 				);
 				);
 
 
 				expect( stringify( root, selection ) ).to.equal(
 				expect( stringify( root, selection ) ).to.equal(
-					'<selection><a></a>foo</selection><$text bold=true>bar</$text><b></b>'
+					'[<a></a>foo]<$text bold="true">bar</$text><b></b>'
 				);
 				);
 			} );
 			} );
 
 
@@ -328,7 +328,7 @@ describe( 'model test utils', () => {
 				);
 				);
 
 
 				expect( stringify( root, selection ) ).to.equal(
 				expect( stringify( root, selection ) ).to.equal(
-					'<a></a>f<selection>o</selection>o<$text bold=true>bar</$text><b></b>'
+					'<a></a>f[o]o<$text bold="true">bar</$text><b></b>'
 				);
 				);
 			} );
 			} );
 
 
@@ -338,18 +338,18 @@ describe( 'model test utils', () => {
 				);
 				);
 
 
 				expect( stringify( root, selection ) ).to.equal(
 				expect( stringify( root, selection ) ).to.equal(
-					'<a><selection></a>foo<$text bold=true>bar</$text><b></selection></b>'
+					'<a>[</a>foo<$text bold="true">bar</$text><b>]</b>'
 				);
 				);
 			} );
 			} );
 
 
-			it( 'writes backward selection', () => {
+			it( 'writes selection when is backward', () => {
 				selection.addRange(
 				selection.addRange(
 					Range.createFromParentsAndOffsets( elA, 0, elB, 0 ),
 					Range.createFromParentsAndOffsets( elA, 0, elB, 0 ),
 					true
 					true
 				);
 				);
 
 
 				expect( stringify( root, selection ) ).to.equal(
 				expect( stringify( root, selection ) ).to.equal(
-					'<a><selection backward></a>foo<$text bold=true>bar</$text><b></selection></b>'
+					'<a>[</a>foo<$text bold="true">bar</$text><b>]</b>'
 				);
 				);
 			} );
 			} );
 
 
@@ -357,7 +357,7 @@ describe( 'model test utils', () => {
 				const range = Range.createFromParentsAndOffsets( elA, 0, elB, 0 );
 				const range = Range.createFromParentsAndOffsets( elA, 0, elB, 0 );
 
 
 				expect( stringify( root, range ) ).to.equal(
 				expect( stringify( root, range ) ).to.equal(
-					'<a><selection></a>foo<$text bold=true>bar</$text><b></selection></b>'
+					'<a>[</a>foo<$text bold="true">bar</$text><b>]</b>'
 				);
 				);
 			} );
 			} );
 
 
@@ -365,7 +365,7 @@ describe( 'model test utils', () => {
 				const position = new Position( root, [ 0 ] );
 				const position = new Position( root, [ 0 ] );
 
 
 				expect( stringify( root, position ) ).to.equal(
 				expect( stringify( root, position ) ).to.equal(
-					'<selection /><a></a>foo<$text bold=true>bar</$text><b></b>'
+					'[]<a></a>foo<$text bold="true">bar</$text><b></b>'
 				);
 				);
 			} );
 			} );
 		} );
 		} );
@@ -380,7 +380,7 @@ describe( 'model test utils', () => {
 		} );
 		} );
 
 
 		test( 'creates empty DocumentFragment with selection', {
 		test( 'creates empty DocumentFragment with selection', {
-			data: '<selection />',
+			data: '[]',
 			check( fragment, selection ) {
 			check( fragment, selection ) {
 				expect( fragment ).to.be.instanceOf( DocumentFragment );
 				expect( fragment ).to.be.instanceOf( DocumentFragment );
 				expect( fragment.childCount ).to.equal( 0 );
 				expect( fragment.childCount ).to.equal( 0 );
@@ -390,7 +390,7 @@ describe( 'model test utils', () => {
 		} );
 		} );
 
 
 		test( 'returns Element if range is around single element', {
 		test( 'returns Element if range is around single element', {
-			data: '<selection><a></a></selection>',
+			data: '[<a></a>]',
 			check( el, selection ) {
 			check( el, selection ) {
 				const fragment = el.parent;
 				const fragment = el.parent;
 				expect( el ).to.be.instanceOf( Element );
 				expect( el ).to.be.instanceOf( Element );
@@ -417,29 +417,24 @@ describe( 'model test utils', () => {
 		} );
 		} );
 
 
 		test( 'sets elements attributes', {
 		test( 'sets elements attributes', {
-			data: '<a foo=1 bar=true car="x y"><b x="y"></b></a>',
-			output: '<a bar=true car="x y" foo=1><b x="y"></b></a>',
+			data: '<a foo="1" bar="true" car="x y"><b x="y"></b></a>',
+			output: '<a bar="true" car="x y" foo="1"><b x="y"></b></a>',
 			check( a ) {
 			check( a ) {
+				expect( a.getAttribute( 'bar' ) ).to.equal( 'true' );
 				expect( a.getAttribute( 'car' ) ).to.equal( 'x y' );
 				expect( a.getAttribute( 'car' ) ).to.equal( 'x y' );
-			}
-		} );
-
-		test( 'sets complex attributes', {
-			data: '<a foo={"a":1,"b":"c"}></a>',
-			check( a ) {
-				expect( a.getAttribute( 'foo' ) ).to.have.property( 'a', 1 );
+				expect( a.getAttribute( 'foo' ) ).to.equal( '1' );
 			}
 			}
 		} );
 		} );
 
 
 		test( 'sets text attributes', {
 		test( 'sets text attributes', {
-			data: '<$text bold=true italic=true>foo</$text><$text bold=true>bar</$text>bom',
+			data: '<$text bold="true" italic="true">foo</$text><$text bold="true">bar</$text>bom',
 			check( root ) {
 			check( root ) {
 				expect( root.childCount ).to.equal( 3 );
 				expect( root.childCount ).to.equal( 3 );
 				expect( root.maxOffset ).to.equal( 9 );
 				expect( root.maxOffset ).to.equal( 9 );
 				expect( root.getChild( 0 ) ).to.have.property( 'data', 'foo' );
 				expect( root.getChild( 0 ) ).to.have.property( 'data', 'foo' );
-				expect( root.getChild( 0 ).getAttribute( 'italic' ) ).to.equal( true );
+				expect( root.getChild( 0 ).getAttribute( 'italic' ) ).to.equal( 'true' );
 				expect( root.getChild( 1 ) ).to.have.property( 'data', 'bar' );
 				expect( root.getChild( 1 ) ).to.have.property( 'data', 'bar' );
-				expect( root.getChild( 1 ).getAttribute( 'bold' ) ).to.equal( true );
+				expect( root.getChild( 1 ).getAttribute( 'bold' ) ).to.equal( 'true' );
 			}
 			}
 		} );
 		} );
 
 
@@ -457,64 +452,34 @@ describe( 'model test utils', () => {
 			}
 			}
 		} );
 		} );
 
 
-		it( 'throws when unexpected closing tag', () => {
+		it( 'throws when invalid XML', () => {
 			expect( () => {
 			expect( () => {
 				parse( '<a><b></a></b>' );
 				parse( '<a><b></a></b>' );
-			} ).to.throw( Error, 'Parse error - unexpected closing tag.' );
-		} );
-
-		it( 'throws when unexpected attribute', () => {
-			expect( () => {
-				parse( '<a ?></a>' );
-			} ).to.throw( Error, 'Parse error - unexpected token: ?.' );
-		} );
-
-		it( 'throws when incorrect tag', () => {
-			expect( () => {
-				parse( '<a' );
-			} ).to.throw( Error, 'Parse error - unexpected token: <a.' );
-		} );
-
-		it( 'throws when missing closing tag', () => {
-			expect( () => {
-				parse( '<a><b></b>' );
-			} ).to.throw( Error, 'Parse error - missing closing tags: a.' );
-		} );
-
-		it( 'throws when missing opening tag for text', () => {
-			expect( () => {
-				parse( '</$text>' );
-			} ).to.throw( Error, 'Parse error - unexpected closing tag.' );
-		} );
-
-		it( 'throws when missing closing tag for text', () => {
-			expect( () => {
-				parse( '<$text>' );
-			} ).to.throw( Error, 'Parse error - missing closing tags: $text.' );
+			} ).to.throw( Error );
 		} );
 		} );
 
 
 		describe( 'selection', () => {
 		describe( 'selection', () => {
 			test( 'sets collapsed selection in an element', {
 			test( 'sets collapsed selection in an element', {
-				data: '<a><selection /></a>',
+				data: '<a>[]</a>',
 				check( root, selection ) {
 				check( root, selection ) {
 					expect( selection.getFirstPosition().parent ).to.have.property( 'name', 'a' );
 					expect( selection.getFirstPosition().parent ).to.have.property( 'name', 'a' );
 				}
 				}
 			} );
 			} );
 
 
 			test( 'sets collapsed selection between elements', {
 			test( 'sets collapsed selection between elements', {
-				data: '<a></a><selection /><b></b>'
+				data: '<a></a>[]<b></b>'
 			} );
 			} );
 
 
 			test( 'sets collapsed selection before a text', {
 			test( 'sets collapsed selection before a text', {
-				data: '<a></a><selection />foo'
+				data: '<a></a>[]foo'
 			} );
 			} );
 
 
 			test( 'sets collapsed selection after a text', {
 			test( 'sets collapsed selection after a text', {
-				data: 'foo<selection />'
+				data: 'foo[]'
 			} );
 			} );
 
 
 			test( 'sets collapsed selection within a text', {
 			test( 'sets collapsed selection within a text', {
-				data: 'foo<selection />bar',
+				data: 'foo[]bar',
 				check( text, selection ) {
 				check( text, selection ) {
 					expect( text.offsetSize ).to.equal( 6 );
 					expect( text.offsetSize ).to.equal( 6 );
 					expect( text.getPath() ).to.deep.equal( [ 0 ] );
 					expect( text.getPath() ).to.deep.equal( [ 0 ] );
@@ -523,15 +488,17 @@ describe( 'model test utils', () => {
 				}
 				}
 			} );
 			} );
 
 
-			test( 'sets selection attributes', {
-				data: 'foo<selection bold=true italic=true />bar',
-				check( root, selection ) {
-					expect( selection.getAttribute( 'italic' ) ).to.be.true;
-				}
+			it( 'sets selection attributes', () => {
+				const result = parse( 'foo[]bar', document.schema, { selectionAttributes: {
+					bold: true,
+					italic: true
+				} } );
+
+				expect( stringify( result.model, result.selection ) ).to.equal( 'foo<$text bold="true" italic="true">[]</$text>bar' );
 			} );
 			} );
 
 
 			test( 'sets collapsed selection between text and text with attributes', {
 			test( 'sets collapsed selection between text and text with attributes', {
-				data: 'foo<selection /><$text bold=true>bar</$text>',
+				data: 'foo[]<$text bold="true">bar</$text>',
 				check( root, selection ) {
 				check( root, selection ) {
 					expect( root.maxOffset ).to.equal( 6 );
 					expect( root.maxOffset ).to.equal( 6 );
 					expect( selection.getAttribute( 'bold' ) ).to.be.undefined;
 					expect( selection.getAttribute( 'bold' ) ).to.be.undefined;
@@ -539,42 +506,57 @@ describe( 'model test utils', () => {
 			} );
 			} );
 
 
 			test( 'sets selection containing an element', {
 			test( 'sets selection containing an element', {
-				data: 'x<selection><a></a></selection>'
+				data: 'x[<a></a>]'
 			} );
 			} );
 
 
-			test( 'sets selection with attribute containing an element', {
-				data: 'x<selection bold=true><a></a></selection>'
+			it( 'sets selection with attribute containing an element', () => {
+				const result = parse( 'x[<a></a>]', document.schema, { selectionAttributes: {
+					bold: true
+				} } );
+
+				expect( stringify( result.model, result.selection ) ).to.equal( 'x[<a></a>]' );
+				expect( result.selection.getAttribute( 'bold' ) ).to.be.true;
 			} );
 			} );
 
 
-			test( 'sets a backward selection containing an element', {
-				data: 'x<selection backward bold=true><a></a></selection>'
+			it( 'sets a backward selection containing an element', () => {
+				const result = parse( 'x[<a></a>]', document.schema, {
+					lastRangeBackward: true
+				} );
+
+				expect( stringify( result.model, result.selection ) ).to.equal( 'x[<a></a>]' );
+				expect( result.selection.isBackward ).to.true;
 			} );
 			} );
 
 
 			test( 'sets selection within a text', {
 			test( 'sets selection within a text', {
-				data: 'x<selection bold=true>y</selection>z'
+				data: 'x[y]z'
 			} );
 			} );
 
 
-			test( 'sets selection within a text with different attributes', {
-				data: '<$text bold=true>fo<selection bold=true>o</$text>ba</selection>r'
+			it( 'sets selection within a text with different attributes', () => {
+				const result = parse( '<$text bold="true">fo[o</$text>ba]r', document.schema, {
+					selectionAttributes: { bold: true }
+				} );
+
+				expect( stringify( result.model, result.selection ) ).to.equal( '<$text bold="true">fo[o</$text>ba]r' );
+				expect( result.selection.getAttribute( 'bold' ) ).to.true;
 			} );
 			} );
 
 
 			it( 'throws when missing selection start', () => {
 			it( 'throws when missing selection start', () => {
 				expect( () => {
 				expect( () => {
-					parse( 'foo</selection>' );
-				} ).to.throw( Error, 'Parse error - missing selection start.' );
+					parse( 'foo]' );
+				} ).to.throw( Error );
 			} );
 			} );
 
 
 			it( 'throws when missing selection end', () => {
 			it( 'throws when missing selection end', () => {
 				expect( () => {
 				expect( () => {
-					parse( '<selection>foo' );
-				} ).to.throw( Error, 'Parse error - missing selection end.' );
+					parse( '[foo' );
+				} ).to.throw( Error );
 			} );
 			} );
 		} );
 		} );
 
 
 		function test( title, options ) {
 		function test( title, options ) {
 			it( title, () => {
 			it( title, () => {
 				const output = options.output || options.data;
 				const output = options.output || options.data;
-				const data = parse( options.data );
+				const data = parse( options.data, document.schema );
 				let model, selection;
 				let model, selection;
 
 
 				if ( data.selection && data.model ) {
 				if ( data.selection && data.model ) {

+ 17 - 4
packages/ckeditor5-engine/tests/_utils-tests/view.js

@@ -187,6 +187,19 @@ describe( 'view test utils', () => {
 			expect( stringify( p, selection ) ).to.equal( '<p><b>f{ooba}r</b><b>bazqux</b></p>' );
 			expect( stringify( p, selection ) ).to.equal( '<p><b>f{ooba}r</b><b>bazqux</b></p>' );
 		} );
 		} );
 
 
+		it( 'should write selection ranges inside text represented by `[` and `]` characters', () => {
+			const text1 = new Text( 'foobar' );
+			const text2 = new Text( 'bazqux' );
+			const b1 = new Element( 'b', null, text1 );
+			const b2 = new Element( 'b', null, text2 );
+			const p = new Element( 'p', null, [ b1, b2 ] );
+			const range = Range.createFromParentsAndOffsets( text1, 1, text1, 5 );
+			const selection = new Selection();
+			selection.addRange( range );
+			expect( stringify( p, selection, { sameSelectionCharacters: true } ) )
+				.to.equal( '<p><b>f[ooba]r</b><b>bazqux</b></p>' );
+		} );
+
 		it( 'should write collapsed selection ranges inside texts', () => {
 		it( 'should write collapsed selection ranges inside texts', () => {
 			const text = new Text( 'foobar' );
 			const text = new Text( 'foobar' );
 			const p = new Element( 'p', null, text );
 			const p = new Element( 'p', null, text );
@@ -228,7 +241,7 @@ describe( 'view test utils', () => {
 			const p = new ContainerElement( 'p', null, b );
 			const p = new ContainerElement( 'p', null, b );
 
 
 			expect( stringify( p, null, { showPriority: true } ) )
 			expect( stringify( p, null, { showPriority: true } ) )
-				.to.equal( '<p><b:10>foobar</b:10></p>' );
+				.to.equal( '<p><b view-priority="10">foobar</b></p>' );
 		} );
 		} );
 
 
 		it( 'should parse DocumentFragment as root', () => {
 		it( 'should parse DocumentFragment as root', () => {
@@ -400,10 +413,10 @@ describe( 'view test utils', () => {
 		} );
 		} );
 
 
 		it( 'should parse element priority', () => {
 		it( 'should parse element priority', () => {
-			const parsed1 = parse( '<b:12></b:12>' );
+			const parsed1 = parse( '<b view-priority="12"></b>' );
 			const attribute1 = new AttributeElement( 'b' );
 			const attribute1 = new AttributeElement( 'b' );
 			attribute1.priority = 12;
 			attribute1.priority = 12;
-			const parsed2 = parse( '<attribute:b:44></attribute:b:44>' );
+			const parsed2 = parse( '<attribute:b view-priority="44"></attribute:b>' );
 			const attribute2 = new AttributeElement( 'b' );
 			const attribute2 = new AttributeElement( 'b' );
 			attribute2.priority = 44;
 			attribute2.priority = 44;
 
 
@@ -413,7 +426,7 @@ describe( 'view test utils', () => {
 		} );
 		} );
 
 
 		it( 'should paste nested elements and texts', () => {
 		it( 'should paste nested elements and texts', () => {
-			const parsed = parse( '<container:p>foo<b:12>bar<i:25>qux</i:25></b:12></container:p>' );
+			const parsed = parse( '<container:p>foo<b view-priority="12">bar<i view-priority="25">qux</i></b></container:p>' );
 			expect( parsed.isSimilar( new ContainerElement( 'p' ) ) ).to.be.true;
 			expect( parsed.isSimilar( new ContainerElement( 'p' ) ) ).to.be.true;
 			expect( parsed.childCount ).to.equal( 2 );
 			expect( parsed.childCount ).to.equal( 2 );
 			expect( parsed.getChild( 0 ) ).to.be.instanceof( Text ).and.have.property( 'data' ).that.equal( 'foo' );
 			expect( parsed.getChild( 0 ) ).to.be.instanceof( Text ).and.have.property( 'data' ).that.equal( 'foo' );

+ 212 - 340
packages/ckeditor5-engine/tests/_utils/model.js

@@ -3,20 +3,39 @@
  * For licensing, see LICENSE.md.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-import TreeWalker from '/ckeditor5/engine/model/treewalker.js';
-import Range from '/ckeditor5/engine/model/range.js';
-import Position from '/ckeditor5/engine/model/position.js';
-import Text from '/ckeditor5/engine/model/text.js';
+import Mapper from '/ckeditor5/engine/conversion/mapper.js';
+import { parse as viewParse, stringify as viewStringify } from '/tests/engine/_utils/view.js';
+import {
+	convertRangeSelection,
+	convertCollapsedSelection,
+	convertSelectionAttribute
+} from '/ckeditor5/engine/conversion/model-selection-to-view-converters.js';
+import { insertText, insertElement, wrap } from '/ckeditor5/engine/conversion/model-to-view-converters.js';
+
 import RootElement from '/ckeditor5/engine/model/rootelement.js';
 import RootElement from '/ckeditor5/engine/model/rootelement.js';
-import Element from '/ckeditor5/engine/model/element.js';
-import DocumentFragment from '/ckeditor5/engine/model/documentfragment.js';
-import Selection from '/ckeditor5/engine/model/selection.js';
-import Document from '/ckeditor5/engine/model/document.js';
-import writer from '/ckeditor5/engine/model/writer.js';
+import ModelDocument from '/ckeditor5/engine/model/document.js';
+import ModelRange from '/ckeditor5/engine/model/range.js';
+import ModelPosition from '/ckeditor5/engine/model/position.js';
+import ModelConversionDispatcher from '/ckeditor5/engine/conversion/modelconversiondispatcher.js';
+import ModelSelection from '/ckeditor5/engine/model/selection.js';
+import ModelDocumentFragment from '/ckeditor5/engine/model/documentfragment.js';
+import ModelElement from '/ckeditor5/engine/model/element.js';
+import ModelText from '/ckeditor5/engine/model/text.js';
+import ModelTextProxy from '/ckeditor5/engine/model/textproxy.js';
+import modelWriter from '/ckeditor5/engine/model/writer.js';
+
+import ViewConversionDispatcher from '/ckeditor5/engine/conversion/viewconversiondispatcher.js';
+import ViewSelection from '/ckeditor5/engine/view/selection.js';
+import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
+import ViewElement from '/ckeditor5/engine/view/containerelement.js';
+import ViewAttributeElement from '/ckeditor5/engine/view/attributeelement.js';
 
 
 /**
 /**
  * Writes the contents of the {@link engine.model.Document Document} to an HTML-like string.
  * Writes the contents of the {@link engine.model.Document Document} to an HTML-like string.
  *
  *
+ * ** Note: ** {@link engine.model.Text text} node contains attributes will be represented as:
+ *        <$text attribute="value">Text data</$text>
+ *
  * @param {engine.model.Document} document
  * @param {engine.model.Document} document
  * @param {Object} [options]
  * @param {Object} [options]
  * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will
  * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will
@@ -26,7 +45,7 @@ import writer from '/ckeditor5/engine/model/writer.js';
  * @returns {String} The stringified data.
  * @returns {String} The stringified data.
  */
  */
 export function getData( document, options = {} ) {
 export function getData( document, options = {} ) {
-	if ( !( document instanceof Document ) ) {
+	if ( !( document instanceof ModelDocument ) ) {
 		throw new TypeError( 'Document needs to be an instance of engine.model.Document.' );
 		throw new TypeError( 'Document needs to be an instance of engine.model.Document.' );
 	}
 	}
 
 
@@ -44,39 +63,53 @@ getData._stringify = stringify;
  * Sets the contents of the {@link engine.model.Document Document} provided as HTML-like string.
  * Sets the contents of the {@link engine.model.Document Document} provided as HTML-like string.
  * It uses {@link engine.model.Document#enqueueChanges enqueueChanges} method.
  * It uses {@link engine.model.Document#enqueueChanges enqueueChanges} method.
  *
  *
- * NOTE:
- * Remember to register elements in {@link engine.model.Document#schema document's schema} before inserting them.
+ * ** Note: ** Remember to register elements in {@link engine.model.Document#schema document's schema} before inserting them.
+ *
+ * ** Note: ** To create {@link engine.model.Text text} node witch containing attributes use:
+ *        <$text attribute="value">Text data</$text>
  *
  *
  * @param {engine.model.Document} document
  * @param {engine.model.Document} document
  * @param {String} data HTML-like string to write into Document.
  * @param {String} data HTML-like string to write into Document.
  * @param {Object} options
  * @param {Object} options
  * @param {String} [options.rootName='main'] Root name where parsed data will be stored. If not provided, default `main`
  * @param {String} [options.rootName='main'] Root name where parsed data will be stored. If not provided, default `main`
  * name will be used.
  * name will be used.
+ * @param {Array<Object>} [options.selectionAttributes] List of attributes which will be passed to the selection.
+ * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward.
  * @param {String} [options.batchType='transparent'] Batch type used for inserting elements. See {@link engine.model.Batch#type}.
  * @param {String} [options.batchType='transparent'] Batch type used for inserting elements. See {@link engine.model.Batch#type}.
  */
  */
 export function setData( document, data, options = {} ) {
 export function setData( document, data, options = {} ) {
-	if ( !( document instanceof Document ) ) {
+	if ( !( document instanceof ModelDocument ) ) {
 		throw new TypeError( 'Document needs to be an instance of engine.model.Document.' );
 		throw new TypeError( 'Document needs to be an instance of engine.model.Document.' );
 	}
 	}
 
 
-	let model, selection;
-	const result = setData._parse( data );
+	let modelDocumentFragment, selection;
+	const modelRoot = document.getRoot( options.rootName || 'main' );
 
 
-	if ( result.model && result.selection ) {
-		model = result.model;
-		selection = result.selection;
+	// Parse data string to model.
+	const parsedResult = setData._parse( data, document.schema, {
+		lastRangeBackward: options.lastRangeBackward,
+		selectionAttributes: options.selectionAttributes
+	} );
+
+	// Retrieve DocumentFragment and Selection from parsed model.
+	if ( parsedResult.model ) {
+		modelDocumentFragment = parsedResult.model;
+		selection = parsedResult.selection;
 	} else {
 	} else {
-		model = result;
+		modelDocumentFragment = parsedResult;
 	}
 	}
 
 
-	// Save to model.
-	const modelRoot = document.getRoot( options.rootName || 'main' );
-
 	document.enqueueChanges( () => {
 	document.enqueueChanges( () => {
+		// Replace existing model in document by new one.
 		document.batch( options.batchType || 'transparent' )
 		document.batch( options.batchType || 'transparent' )
-			.remove( Range.createIn( modelRoot ) )
-			.insert( Position.createAt( modelRoot, 0 ), model );
+			.remove( ModelRange.createIn( modelRoot ) )
+			.insert( ModelPosition.createAt( modelRoot, 0 ), modelDocumentFragment );
+
+		// Clean up previous document selection.
+		document.selection.clearAttributes();
+		document.selection.removeAllRanges();
 
 
+		// Update document selection if specified.
 		if ( selection ) {
 		if ( selection ) {
 			const ranges = [];
 			const ranges = [];
 
 
@@ -86,22 +119,23 @@ export function setData( document, data, options = {} ) {
 				// Each range returned from `parse()` method has its root placed in DocumentFragment.
 				// Each range returned from `parse()` method has its root placed in DocumentFragment.
 				// Here we convert each range to have its root re-calculated properly and be placed inside
 				// Here we convert each range to have its root re-calculated properly and be placed inside
 				// model document root.
 				// model document root.
-				if ( range.start.parent instanceof DocumentFragment ) {
-					start = Position.createFromParentAndOffset( modelRoot, range.start.offset );
+				if ( range.start.parent instanceof ModelDocumentFragment ) {
+					start = ModelPosition.createFromParentAndOffset( modelRoot, range.start.offset );
 				} else {
 				} else {
-					start = Position.createFromParentAndOffset( range.start.parent, range.start.offset );
+					start = ModelPosition.createFromParentAndOffset( range.start.parent, range.start.offset );
 				}
 				}
 
 
-				if ( range.end.parent instanceof DocumentFragment ) {
-					end = Position.createFromParentAndOffset( modelRoot, range.end.offset );
+				if ( range.end.parent instanceof ModelDocumentFragment ) {
+					end = ModelPosition.createFromParentAndOffset( modelRoot, range.end.offset );
 				} else {
 				} else {
-					end = Position.createFromParentAndOffset( range.end.parent, range.end.offset );
+					end = ModelPosition.createFromParentAndOffset( range.end.parent, range.end.offset );
 				}
 				}
 
 
-				ranges.push( new Range( start, end ) );
+				ranges.push( new ModelRange( start, end ) );
 			}
 			}
 
 
 			document.selection.setRanges( ranges, selection.isBackward );
 			document.selection.setRanges( ranges, selection.isBackward );
+			document.selection.setAttributesTo( selection.getAttributes() );
 		}
 		}
 	} );
 	} );
 }
 }
@@ -112,393 +146,231 @@ setData._parse = parse;
 /**
 /**
  * Converts model nodes to HTML-like string representation.
  * Converts model nodes to HTML-like string representation.
  *
  *
+ * ** Note: ** {@link engine.model.Text text} node contains attributes will be represented as:
+ *        <$text attribute="value">Text data</$text>
+ *
  * @param {engine.model.RootElement|engine.model.Element|engine.model.Text|
  * @param {engine.model.RootElement|engine.model.Element|engine.model.Text|
  * engine.model.DocumentFragment} node Node to stringify.
  * engine.model.DocumentFragment} node Node to stringify.
- * @param {engine.model.Selection|engine.model.Position|engine.model.Range} [selectionOrPositionOrRange = null ]
+ * @param {engine.model.Selection|engine.model.Position|engine.model.Range} [selectionOrPositionOrRange=null]
  * Selection instance which ranges will be included in returned string data. If Range instance is provided - it will be
  * Selection instance which ranges will be included in returned string data. If Range instance is provided - it will be
  * converted to selection containing this range. If Position instance is provided - it will be converted to selection
  * converted to selection containing this range. If Position instance is provided - it will be converted to selection
  * containing one range collapsed at this position.
  * containing one range collapsed at this position.
  * @returns {String} HTML-like string representing the model.
  * @returns {String} HTML-like string representing the model.
  */
  */
 export function stringify( node, selectionOrPositionOrRange = null ) {
 export function stringify( node, selectionOrPositionOrRange = null ) {
+	const mapper = new Mapper();
 	let selection, range;
 	let selection, range;
 
 
-	if ( node instanceof RootElement || node instanceof DocumentFragment ) {
-		range = Range.createIn( node );
+	// Create a range witch wraps passed node.
+	if ( node instanceof RootElement || node instanceof ModelDocumentFragment ) {
+		range = ModelRange.createIn( node );
 	} else {
 	} else {
 		// Node is detached - create new document fragment.
 		// Node is detached - create new document fragment.
 		if ( !node.parent ) {
 		if ( !node.parent ) {
-			const fragment = new DocumentFragment( node );
-			range = Range.createIn( fragment );
+			const fragment = new ModelDocumentFragment( node );
+			range = ModelRange.createIn( fragment );
 		} else {
 		} else {
-			range = new Range(
-				Position.createBefore( node ),
-				Position.createAfter( node )
+			range = new ModelRange(
+				ModelPosition.createBefore( node ),
+				ModelPosition.createAfter( node )
 			);
 			);
 		}
 		}
 	}
 	}
 
 
-	const walker = new TreeWalker( {
-		boundaries: range
-	} );
-
-	if ( selectionOrPositionOrRange instanceof Selection ) {
+	// Get selection from passed selection or position or range if at least one is specified.
+	if ( selectionOrPositionOrRange instanceof ModelSelection ) {
 		selection = selectionOrPositionOrRange;
 		selection = selectionOrPositionOrRange;
-	} else if ( selectionOrPositionOrRange instanceof Range ) {
-		selection = new Selection();
+	} else if ( selectionOrPositionOrRange instanceof ModelRange ) {
+		selection = new ModelSelection();
 		selection.addRange( selectionOrPositionOrRange );
 		selection.addRange( selectionOrPositionOrRange );
-	} else if ( selectionOrPositionOrRange instanceof Position ) {
-		selection = new Selection();
-		selection.addRange( new Range( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
+	} else if ( selectionOrPositionOrRange instanceof ModelPosition ) {
+		selection = new ModelSelection();
+		selection.addRange( new ModelRange( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
 	}
 	}
 
 
-	let ret = '';
-	let lastPosition = Position.createFromPosition( range.start );
-	const withSelection = !!selection;
-
-	for ( let value of walker ) {
-		if ( withSelection ) {
-			ret += writeSelection( value.previousPosition, selection );
-		}
+	// Setup model to view converter.
+	const viewDocumentFragment = new ViewDocumentFragment();
+	const viewSelection = new ViewSelection();
+	const modelToView = new ModelConversionDispatcher( { mapper, viewSelection } );
 
 
-		ret += writeItem( value, selection, { selection: withSelection } );
+	// Bind root elements.
+	mapper.bindElements( node.root, viewDocumentFragment );
 
 
-		lastPosition = value.nextPosition;
+	modelToView.on( 'insert:$text', insertText(), 'lowest' );
+	modelToView.on( 'addAttribute', wrap( ( value, data ) => {
+		if ( data.item instanceof ModelTextProxy ) {
+			return new ViewAttributeElement( 'model-text-with-attributes', { [ data.attributeKey ]: value } );
+		}
+	} ), 'lowest' );
+	modelToView.on( 'insert', insertElement( data => new ViewElement( data.item.name, data.item.getAttributes() )  ), 'lowest' );
+	modelToView.on( 'selection', convertRangeSelection(), 'lowest' );
+	modelToView.on( 'selection', convertCollapsedSelection(), 'lowest' );
+	modelToView.on( 'selectionAttribute', convertSelectionAttribute( ( value, data ) => {
+		return new ViewAttributeElement( 'model-text-with-attributes', { [ data.key ]: value } );
+	} ), 'lowest' );
+
+	// Convert model to view.
+	modelToView.convertInsertion( range );
+
+	// Convert model selection to view selection.
+	if ( selection ) {
+		modelToView.convertSelection( selection );
 	}
 	}
 
 
-	if ( withSelection ) {
-		ret += writeSelection( lastPosition, selection );
-	}
+	// Parse view to data string.
+	let data = viewStringify( viewDocumentFragment, viewSelection, { sameSelectionCharacters: true } );
 
 
-	return ret;
+	// Replace valid XML `model-text-with-attributes` element name to `$text`.
+	return data.replace( new RegExp( 'model-text-with-attributes', 'g' ), '$text' );
 }
 }
 
 
 /**
 /**
  * Parses HTML-like string and returns model {@link engine.model.RootElement rootElement}.
  * Parses HTML-like string and returns model {@link engine.model.RootElement rootElement}.
  *
  *
+ * ** Note: ** To create {@link engine.model.Text text} node witch containing attributes use:
+ *        <$text attribute="value">Text data</$text>
+ *
  * @param {String} data HTML-like string to be parsed.
  * @param {String} data HTML-like string to be parsed.
- * @param {Object} options
+ * @param {engine.model.schema} schema Schema instance uses by converters for element validation.
+ * @param {Object} options Additional configuration.
+ * @param {Array<Object>} [options.selectionAttributes] List of attributes which will be passed to the selection.
+ * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward.
  * @returns {engine.model.Element|engine.model.Text|engine.model.DocumentFragment|Object} Returns parsed model node or
  * @returns {engine.model.Element|engine.model.Text|engine.model.DocumentFragment|Object} Returns parsed model node or
  * object with two fields `model` and `selection` when selection ranges were included in data to parse.
  * object with two fields `model` and `selection` when selection ranges were included in data to parse.
  */
  */
-export function parse( data ) {
-	let root, selection;
-	let withSelection = false;
-
-	root = new DocumentFragment();
-	selection = new Selection();
-
-	const path = [];
-	let selectionStart, selectionEnd, selectionAttributes, textAttributes;
-
-	const handlers = {
-		text( token ) {
-			writer.insert( Position.createFromParentAndOffset( root, root.maxOffset ), new Text( token.data, textAttributes ) );
-		},
-
-		textStart( token ) {
-			textAttributes = token.attributes;
-			path.push( '$text' );
-		},
-
-		textEnd() {
-			if ( path.pop() != '$text' ) {
-				throw new Error( 'Parse error - unexpected closing tag.' );
-			}
-
-			textAttributes = null;
-		},
-
-		openingTag( token ) {
-			let el = new Element( token.name, token.attributes );
-			writer.insert( Position.createFromParentAndOffset( root, root.maxOffset ), el );
-
-			root = el;
+export function parse( data, schema, options = {} ) {
+	const mapper = new Mapper();
 
 
-			path.push( token.name );
-		},
+	// Replace not accepted by XML `$text` tag name by valid one `model-text-with-attributes`.
+	data = data.replace( new RegExp( '\\$text', 'g' ), 'model-text-with-attributes' );
 
 
-		closingTag( token ) {
-			if ( path.pop() != token.name ) {
-				throw new Error( 'Parse error - unexpected closing tag.' );
-			}
-
-			root = root.parent;
-		},
-
-		collapsedSelection( token ) {
-			withSelection = true;
-			selection.collapse( root, 'end' );
-			selection.setAttributesTo( token.attributes );
-		},
-
-		selectionStart( token ) {
-			selectionStart = Position.createFromParentAndOffset( root, root.maxOffset );
-			selectionAttributes = token.attributes;
-		},
-
-		selectionEnd() {
-			if ( !selectionStart ) {
-				throw new Error( 'Parse error - missing selection start.' );
-			}
-
-			withSelection = true;
-			selectionEnd = Position.createFromParentAndOffset( root, root.maxOffset );
-
-			selection.setRanges(
-				[ new Range( selectionStart, selectionEnd ) ],
-				selectionAttributes.backward
-			);
+	// Parse data to view using view utils.
+	const parsedResult = viewParse( data, {
+		sameSelectionCharacters: true,
+		lastRangeBackward: !!options.lastRangeBackward
+	} );
 
 
-			delete selectionAttributes.backward;
+	// Retrieve DocumentFragment and Selection from parsed view.
+	let viewDocumentFragment, viewSelection;
 
 
-			selection.setAttributesTo( selectionAttributes );
-		}
-	};
-
-	for ( let token of tokenize( data ) ) {
-		handlers[ token.type ]( token );
+	if ( parsedResult.view && parsedResult.selection ) {
+		viewDocumentFragment = parsedResult.view;
+		viewSelection = parsedResult.selection;
+	} else {
+		viewDocumentFragment = parsedResult;
 	}
 	}
 
 
-	if ( path.length ) {
-		throw new Error( 'Parse error - missing closing tags: ' + path.join( ', ' ) + '.' );
-	}
+	// Setup view to model converter.
+	const viewToModel = new ViewConversionDispatcher( { schema, mapper } );
 
 
-	if ( selectionStart && !selectionEnd ) {
-		throw new Error( 'Parse error - missing selection end.' );
-	}
+	viewToModel.on( 'documentFragment', convertToModelFragment(), 'lowest' );
+	viewToModel.on( `element:model-text-with-attributes`, convertToModelText( true ), 'lowest' );
+	viewToModel.on( 'element', convertToModelElement(), 'lowest' );
+	viewToModel.on( 'text', convertToModelText(), 'lowest' );
 
 
-	// If root DocumentFragment contains only one element - return that element.
-	if ( root instanceof DocumentFragment && root.childCount == 1 ) {
-		root = root.getChild( 0 );
-	}
+	// Convert view to model.
+	let model = viewToModel.convert( viewDocumentFragment.root, { context: [ '$root' ] } );
 
 
-	if ( withSelection ) {
-		return {
-			model: root,
-			selection: selection
-		};
+	// If root DocumentFragment contains only one element - return that element.
+	if ( model instanceof ModelDocumentFragment && model.childCount == 1 ) {
+		model = model.getChild( 0 );
 	}
 	}
 
 
-	return root;
-}
+	// Convert view selection to model selection.
+	let selection;
 
 
-// -- getData helpers ---------------------------------------------------------
+	if ( viewSelection ) {
+		const ranges = [];
 
 
-function writeItem( walkerValue, selection, options ) {
-	const type = walkerValue.type;
-	const item = walkerValue.item;
-
-	if ( type == 'elementStart' ) {
-		let attrs = writeAttributes( item.getAttributes() );
-
-		if ( attrs ) {
-			return `<${ item.name } ${ attrs }>`;
+		// Convert ranges.
+		for ( let viewRange of viewSelection.getRanges() ) {
+			ranges.push( ( mapper.toModelRange( viewRange ) ) );
 		}
 		}
 
 
-		return `<${ item.name }>`;
-	}
-
-	if ( type == 'elementEnd' ) {
-		return `</${ item.name }>`;
-	}
-
-	return writeText( walkerValue, selection, options );
-}
-
-function writeText( walkerValue, selection, options ) {
-	const item = walkerValue.item;
-	const attrs = writeAttributes( item.getAttributes() );
-	let text = Array.from( item.data );
-
-	if ( options.selection ) {
-		const startIndex = walkerValue.previousPosition.offset + 1;
-		const endIndex = walkerValue.nextPosition.offset - 1;
-		let index = startIndex;
-
-		while ( index <= endIndex ) {
-			// Add the selection marker without changing any indexes, so if second marker must be added
-			// in the same loop it does not blow up.
-			text[ index - startIndex ] +=
-				writeSelection( Position.createFromParentAndOffset( item.parent, index ), selection );
+		// Create new selection.
+		selection = new ModelSelection();
+		selection.setRanges( ranges, viewSelection.isBackward );
 
 
-			index++;
+		// Set attributes to selection if specified.
+		if ( options.selectionAttributes ) {
+			selection.setAttributesTo( options.selectionAttributes );
 		}
 		}
 	}
 	}
 
 
-	text = text.join( '' );
-
-	if ( attrs ) {
-		return `<$text ${ attrs }>${ text }</$text>`;
+	// Return model end selection when selection was specified.
+	if ( selection ) {
+		return { model, selection };
 	}
 	}
 
 
-	return text;
-}
-
-function writeAttributes( attrs ) {
-	attrs = Array.from( attrs );
-
-	return attrs.map( attr => attr[ 0 ] + '=' + JSON.stringify( attr[ 1 ] ) ).sort().join( ' ' );
+	// Otherwise return model only.
+	return model;
 }
 }
 
 
-function writeSelection( currentPosition, selection ) {
-	// TODO: This function obviously handles only the first range.
-	const range = selection.getFirstRange();
-
-	// Handle end of the selection.
-	if ( !selection.isCollapsed && range.end.compareWith( currentPosition ) == 'same' ) {
-		return '</selection>';
-	}
+// -- converters view -> model -----------------------------------------------------
 
 
-	// Handle no match.
-	if ( range.start.compareWith( currentPosition ) != 'same' ) {
-		return '';
-	}
+function convertToModelFragment() {
+	return ( evt, data, consumable, conversionApi ) => {
+		// Second argument in `consumable.test` is discarded for ViewDocumentFragment but is needed for ViewElement.
+		if ( !data.output && consumable.test( data.input, { name: true } ) ) {
+			const convertedChildren = conversionApi.convertChildren( data.input, consumable, data );
 
 
-	// Handle beginning of the selection.
-
-	let ret = '<selection';
-	const attrs = writeAttributes( selection.getAttributes() );
-
-	// TODO: Once we'll support multiple ranges this will need to check which range it is.
-	if ( selection.isBackward ) {
-		ret += ' backward';
-	}
-
-	if ( attrs ) {
-		ret += ' ' + attrs;
-	}
-
-	ret += ( selection.isCollapsed ? ' />' : '>' );
-
-	return ret;
-}
-
-// -- setData helpers ---------------------------------------------------------
-
-const patterns = {
-	selection: /^<(\/?selection)( [^>]*)?>/,
-	tag: /^<([^>]+)>/,
-	text: /^[^<]+/
-};
-
-const handlers = {
-	selection( match ) {
-		const tagName = match[ 1 ];
-		const tagExtension = match[ 2 ] || '';
-
-		if ( tagName[ 0 ] == '/' ) {
-			return {
-				type: 'selectionEnd'
-			};
+			data.output = new ModelDocumentFragment( modelWriter.normalizeNodes( convertedChildren ) );
+			conversionApi.mapper.bindElements( data.output, data.input );
 		}
 		}
 
 
-		if ( tagExtension.endsWith( ' /' ) ) {
-			return {
-				type: 'collapsedSelection',
-				attributes: parseAttributes( tagExtension.slice( 1, -2 ) )
-			};
-		}
+		evt.stop();
+	};
+}
 
 
-		return {
-			type: 'selectionStart',
-			attributes: parseAttributes( tagExtension.slice( 1 ) )
+function convertToModelElement() {
+	return ( evt, data, consumable, conversionApi ) => {
+		const schemaQuery = {
+			name: data.input.name,
+			inside: data.context
 		};
 		};
-	},
-
-	tag( match ) {
-		const tagContents = match[ 1 ].split( /\s+/ );
-		const tagName = tagContents.shift();
-		const attrs = tagContents.join( ' ' );
 
 
-		if ( tagName == '/$text' ) {
-			return {
-				type: 'textEnd'
-			};
+		if ( !conversionApi.schema.check( schemaQuery ) ) {
+			throw new Error( `Element '${ schemaQuery.name }' not allowed in context.` );
 		}
 		}
 
 
-		if ( tagName == '$text' ) {
-			return {
-				type: 'textStart',
-				attributes: parseAttributes( attrs )
-			};
-		}
+		if ( consumable.consume( data.input, { name: true } ) ) {
+			data.output = new ModelElement( data.input.name, data.input.getAttributes() );
+			conversionApi.mapper.bindElements( data.output, data.input );
 
 
-		if ( tagName[ 0 ] == '/' ) {
-			return {
-				type: 'closingTag',
-				name: tagName.slice( 1 )
-			};
+			data.context.push( data.output );
+			data.output.appendChildren( conversionApi.convertChildren( data.input, consumable, data ) );
+			data.context.pop();
 		}
 		}
 
 
-		return {
-			type: 'openingTag',
-			name: tagName,
-			attributes: parseAttributes( attrs )
-		};
-	},
-
-	text( match ) {
-		return {
-			type: 'text',
-			data: match[ 0 ]
-		};
-	}
-};
-
-function *tokenize( data ) {
-	while ( data ) {
-		const consumed = consumeNextToken( data );
-
-		data = consumed.data;
-		yield consumed.token;
-	}
+		evt.stop();
+	};
 }
 }
 
 
-function consumeNextToken( data ) {
-	let match;
-
-	for ( let patternName in patterns ) {
-		match = data.match( patterns[ patternName ] );
-
-		if ( match ) {
-			data = data.slice( match[ 0 ].length );
+function convertToModelText( withAttributes = false ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		const schemaQuery = {
+			name: '$text',
+			inside: data.context
+		};
 
 
-			return {
-				token: handlers[ patternName ]( match ),
-				data
-			};
+		if ( !conversionApi.schema.check( schemaQuery ) ) {
+			throw new Error( `Element '${ schemaQuery.name }' not allowed in context.` );
 		}
 		}
-	}
 
 
-	throw new Error( 'Parse error - unexpected token: ' + data + '.' );
-}
+		if ( conversionApi.schema.check( schemaQuery ) ) {
+			if ( consumable.consume( data.input, { name: true } ) ) {
+				let node;
 
 
-function parseAttributes( attrsString ) {
-	attrsString = attrsString.trim();
-
-	if ( !attrsString  ) {
-		return {};
-	}
-
-	const pattern = /(?:backward|(\w+)=("[^"]+"|[^\s]+))\s*/;
-	const attrs = {};
-
-	while ( attrsString ) {
-		let match = attrsString.match( pattern );
-
-		if ( !match ) {
-			throw new Error( 'Parse error - unexpected token: ' + attrsString + '.' );
-		}
+				if ( withAttributes ) {
+					node = new ModelText( data.input.getChild( 0 ).data, data.input.getAttributes() );
+				} else {
+					node = new ModelText( data.input.data );
+				}
 
 
-		if ( match[ 0 ].trim() == 'backward' ) {
-			attrs.backward = true;
-		} else {
-			attrs[ match[ 1 ] ] = JSON.parse( match[ 2 ] );
+				data.output = node;
+			}
 		}
 		}
 
 
-		attrsString = attrsString.slice( match[ 0 ].length );
-	}
-
-	return attrs;
+		evt.stop();
+	};
 }
 }

+ 77 - 55
packages/ckeditor5-engine/tests/_utils/view.js

@@ -5,7 +5,7 @@
 
 
 import Document from '/ckeditor5/engine/view/document.js';
 import Document from '/ckeditor5/engine/view/document.js';
 import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
-import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';
+import XmlDataProcessor from '/ckeditor5/engine/dataprocessor/xmldataprocessor.js';
 import ViewElement from '/ckeditor5/engine/view/element.js';
 import ViewElement from '/ckeditor5/engine/view/element.js';
 import Selection from '/ckeditor5/engine/view/selection.js';
 import Selection from '/ckeditor5/engine/view/selection.js';
 import Range from '/ckeditor5/engine/view/range.js';
 import Range from '/ckeditor5/engine/view/range.js';
@@ -31,7 +31,7 @@ const TEXT_RANGE_END_TOKEN = '}';
  * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed (`<container:p>`
  * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed (`<container:p>`
  * instead of `<p>` and `<attribute:b>` instead of `<b>`).
  * instead of `<p>` and `<attribute:b>` instead of `<b>`).
  * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed
  * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed
- * (`<span:12>`, `<b:10>`).
+ * (`<span view-priority="12">`, `<b view-priority="10">`).
  * @returns {String} The stringified data.
  * @returns {String} The stringified data.
  */
  */
 export function getData( document, options = {} ) {
 export function getData( document, options = {} ) {
@@ -125,6 +125,10 @@ setData._parse = parse;
  *
  *
  *		stringify( p, selection ); // '<p><b>f{ooba}r</b></p>'
  *		stringify( p, selection ); // '<p><b>f{ooba}r</b></p>'
  *
  *
+ * ** Note: **
+ * It is possible to unify selection markers to `[` and `]` for both (inside and outside text)
+ * by setting `sameSelectionCharacters=true` option. It is mainly used when view stringify option is used by model utils.
+ *
  * Multiple ranges are supported:
  * Multiple ranges are supported:
  *
  *
  *		const text = new Text( 'foobar' );
  *		const text = new Text( 'foobar' );
@@ -161,7 +165,7 @@ setData._parse = parse;
  *
  *
  *		const attribute = new AttributeElement( 'b' );
  *		const attribute = new AttributeElement( 'b' );
  *		attribute.priority = 20;
  *		attribute.priority = 20;
- *		getData( attribute, null, { showPriority: true } ); // <b:20></b:20>
+ *		getData( attribute, null, { showPriority: true } ); // <b view-priority="20"></b>
  *
  *
  * @param {engine.view.Text|engine.view.Element|engine.view.DocumentFragment} node Node to stringify.
  * @param {engine.view.Text|engine.view.Element|engine.view.DocumentFragment} node Node to stringify.
  * @param {engine.view.Selection|engine.view.Position|engine.view.Range} [selectionOrPositionOrRange = null ]
  * @param {engine.view.Selection|engine.view.Position|engine.view.Range} [selectionOrPositionOrRange = null ]
@@ -172,9 +176,11 @@ setData._parse = parse;
  * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed (`<container:p>`
  * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed (`<container:p>`
  * instead of `<p>` and `<attribute:b>` instead of `<b>`).
  * instead of `<p>` and `<attribute:b>` instead of `<b>`).
  * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed
  * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed
- * (`<span:12>`, `<b:10>`).
+ * (`<span view-priority="12">`, `<b view-priority="10">`).
  * @param {Boolean} [options.ignoreRoot=false] When set to `true` root's element opening and closing will not be printed.
  * @param {Boolean} [options.ignoreRoot=false] When set to `true` root's element opening and closing will not be printed.
  * Mainly used by `getData` function to ignore {@link engine.view.Document Document's} root element.
  * Mainly used by `getData` function to ignore {@link engine.view.Document Document's} root element.
+ * @param {Boolean} [options.sameSelectionCharacters=false] When set to `true` then selection inside text will be marked as `{` and `}`
+ * and selection outside text as `[` and `]`. When set to `false` then both will be marked as `[` and `]` only.
  * @returns {String} HTML-like string representing the view.
  * @returns {String} HTML-like string representing the view.
  */
  */
 export function stringify( node, selectionOrPositionOrRange = null, options = {} ) {
 export function stringify( node, selectionOrPositionOrRange = null, options = {} ) {
@@ -219,6 +225,10 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
  *
  *
  *		const { root, selection } = parse( '<p>[<b>foobar</b>]</p>' );
  *		const { root, selection } = parse( '<p>[<b>foobar</b>]</p>' );
  *
  *
+ * ** Note: **
+ * It is possible to unify selection markers to `[` and `]` for both (inside and outside text)
+ * by setting `sameSelectionCharacters=true` option. It is mainly used when view parse option is used by model utils.
+ *
  * Sometimes there is a need for defining order of ranges inside created selection. This can be achieved by providing
  * Sometimes there is a need for defining order of ranges inside created selection. This can be achieved by providing
  * ranges order array as additional parameter:
  * ranges order array as additional parameter:
  *
  *
@@ -255,13 +265,19 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
  * When set to `null` root element will be created automatically. If set to
  * When set to `null` root element will be created automatically. If set to
  * {@link engine.view.Element Element} or {@link engine.view.DocumentFragment DocumentFragment} - this node
  * {@link engine.view.Element Element} or {@link engine.view.DocumentFragment DocumentFragment} - this node
  * will be used as root for all parsed nodes.
  * will be used as root for all parsed nodes.
+ * @param {Boolean} [options.sameSelectionCharacters=false] When set to `false` then selection inside text should be marked using
+ * `{` and `}` and selection outside text using `[` and `]`. When set to `true` then both should be marked with `[` and `]` only.
  * @returns {engine.view.Text|engine.view.Element|engine.view.DocumentFragment|Object} Returns parsed view node
  * @returns {engine.view.Text|engine.view.Element|engine.view.DocumentFragment|Object} Returns parsed view node
  * or object with two fields `view` and `selection` when selection ranges were included in data to parse.
  * or object with two fields `view` and `selection` when selection ranges were included in data to parse.
  */
  */
 export function parse( data, options = {} ) {
 export function parse( data, options = {} ) {
 	options.order = options.order || [];
 	options.order = options.order || [];
-	const rangeParser = new RangeParser();
-	const processor = new HtmlDataProcessor();
+	const rangeParser = new RangeParser( {
+		sameSelectionCharacters: options.sameSelectionCharacters
+	} );
+	const processor = new XmlDataProcessor( {
+		namespaces: [ 'attribute', 'container' ]
+	} );
 
 
 	// Convert data to view.
 	// Convert data to view.
 	let view = processor.toView( data );
 	let view = processor.toView( data );
@@ -314,6 +330,18 @@ export function parse( data, options = {} ) {
  * @private
  * @private
  */
  */
 class RangeParser {
 class RangeParser {
+
+	/**
+	 * Create RangeParser instance.
+	 *
+	 * @param {Object} options RangeParser configuration.
+	 * @param {Boolean} [options.sameSelectionCharacters=false] When set to `true` it means that selection inside text is marked as
+	 * `{` and `}` and selection outside text as `[` and `]`. When set to `false` then both are marked as `[` and `]`.
+	 */
+	constructor( options ) {
+		this.sameSelectionCharacters = !!options.sameSelectionCharacters;
+	}
+
 	/**
 	/**
 	 * Parses the view, and returns ranges represented inside {@link engine.view.Text Text nodes}.
 	 * Parses the view, and returns ranges represented inside {@link engine.view.Text Text nodes}.
 	 * Method will remove all occurrences of `{`, `}`, `[` and `]` from found text nodes. If text node is empty after
 	 * Method will remove all occurrences of `{`, `}`, `[` and `]` from found text nodes. If text node is empty after
@@ -337,7 +365,7 @@ class RangeParser {
 		if ( order.length ) {
 		if ( order.length ) {
 			if ( order.length != ranges.length ) {
 			if ( order.length != ranges.length ) {
 				throw new Error(
 				throw new Error(
-					`Parse error - there are ${ ranges.length} ranges found, but ranges order array contains ${ order.length } elements.`
+					`Parse error - there are ${ ranges.length } ranges found, but ranges order array contains ${ order.length } elements.`
 				);
 				);
 			}
 			}
 
 
@@ -383,11 +411,13 @@ class RangeParser {
 
 
 				brackets.push( {
 				brackets.push( {
 					bracket: bracket,
 					bracket: bracket,
-					textOffset: index - offset
+					textOffset: index - offset,
+					outer: index === 0 || index == node._data.length - 1
 				} );
 				} );
 
 
 				offset++;
 				offset++;
 			}
 			}
+
 			text = text.replace( regexp, '' );
 			text = text.replace( regexp, '' );
 			node.data = text;
 			node.data = text;
 			const index = node.index;
 			const index = node.index;
@@ -401,7 +431,10 @@ class RangeParser {
 			for ( let item of brackets ) {
 			for ( let item of brackets ) {
 				// Non-empty text node.
 				// Non-empty text node.
 				if ( text ) {
 				if ( text ) {
-					if ( item.bracket == TEXT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN ) {
+					if (
+						( this.sameSelectionCharacters && !item.outer ) ||
+						( !this.sameSelectionCharacters && ( item.bracket == TEXT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN ) )
+					) {
 						// Store information about text range delimiter.
 						// Store information about text range delimiter.
 						this._positions.push( {
 						this._positions.push( {
 							bracket: item.bracket,
 							bracket: item.bracket,
@@ -409,7 +442,7 @@ class RangeParser {
 						} );
 						} );
 					} else {
 					} else {
 						// Check if element range delimiter is not placed inside text node.
 						// Check if element range delimiter is not placed inside text node.
-						if ( item.textOffset !== 0 && item.textOffset !== text.length ) {
+						if ( !this.sameSelectionCharacters && item.textOffset !== 0 && item.textOffset !== text.length ) {
 							throw new Error( `Parse error - range delimiter '${ item.bracket }' is placed inside text node.` );
 							throw new Error( `Parse error - range delimiter '${ item.bracket }' is placed inside text node.` );
 						}
 						}
 
 
@@ -423,7 +456,7 @@ class RangeParser {
 						} );
 						} );
 					}
 					}
 				} else {
 				} else {
-					if ( item.bracket == TEXT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN ) {
+					if ( !this.sameSelectionCharacters && item.bracket == TEXT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_END_TOKEN ) {
 						throw new Error( `Parse error - text range delimiter '${ item.bracket }' is placed inside empty text node. ` );
 						throw new Error( `Parse error - text range delimiter '${ item.bracket }' is placed inside empty text node. ` );
 					}
 					}
 
 
@@ -520,6 +553,8 @@ class ViewStringify {
 	 * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed.
 	 * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed.
 	 * @param {Boolean} [options.ignoreRoot=false] When set to `true` root's element opening and closing tag will not
 	 * @param {Boolean} [options.ignoreRoot=false] When set to `true` root's element opening and closing tag will not
 	 * be outputted.
 	 * be outputted.
+	 * @param {Boolean} [options.sameSelectionCharacters=false] When set to `true` it means that selection inside text is marked as
+	 * `{` and `}` and selection outside text as `[` and `]`. When set to `false` then both are marked as `[` and `]`.
 	 */
 	 */
 	constructor( root, selection = null, options = {} ) {
 	constructor( root, selection = null, options = {} ) {
 		this.root = root;
 		this.root = root;
@@ -533,6 +568,7 @@ class ViewStringify {
 		this.showType = !!options.showType;
 		this.showType = !!options.showType;
 		this.showPriority = !!options.showPriority;
 		this.showPriority = !!options.showPriority;
 		this.ignoreRoot = !!options.ignoreRoot;
 		this.ignoreRoot = !!options.ignoreRoot;
+		this.sameSelectionCharacters = !!options.sameSelectionCharacters;
 	}
 	}
 
 
 	/**
 	/**
@@ -626,6 +662,15 @@ class ViewStringify {
 	_stringifyTextRanges( node ) {
 	_stringifyTextRanges( node ) {
 		const length = node.data.length;
 		const length = node.data.length;
 		let result = node.data.split( '' );
 		let result = node.data.split( '' );
+		let rangeStartToken, rangeEndToken;
+
+		if ( this.sameSelectionCharacters ) {
+			rangeStartToken = ELEMENT_RANGE_START_TOKEN;
+			rangeEndToken = ELEMENT_RANGE_END_TOKEN;
+		} else {
+			rangeStartToken = TEXT_RANGE_START_TOKEN;
+			rangeEndToken = TEXT_RANGE_END_TOKEN;
+		}
 
 
 		// Add one more element for ranges ending after last character in text.
 		// Add one more element for ranges ending after last character in text.
 		result[ length ] = '';
 		result[ length ] = '';
@@ -646,14 +691,14 @@ class ViewStringify {
 
 
 			if ( start.parent == node && start.offset >= 0 && start.offset <= length ) {
 			if ( start.parent == node && start.offset >= 0 && start.offset <= length ) {
 				if ( range.isCollapsed ) {
 				if ( range.isCollapsed ) {
-					result[ end.offset ].collapsed += TEXT_RANGE_START_TOKEN + TEXT_RANGE_END_TOKEN;
+					result[ end.offset ].collapsed += rangeStartToken + rangeEndToken;
 				} else {
 				} else {
-					result[ start.offset ].start += TEXT_RANGE_START_TOKEN;
+					result[ start.offset ].start += rangeStartToken;
 				}
 				}
 			}
 			}
 
 
 			if ( end.parent == node && end.offset >= 0 && end.offset <= length && !range.isCollapsed  ) {
 			if ( end.parent == node && end.offset >= 0 && end.offset <= length && !range.isCollapsed  ) {
-				result[ end.offset ].end += TEXT_RANGE_END_TOKEN;
+				result[ end.offset ].end += rangeEndToken;
 			}
 			}
 		}
 		}
 
 
@@ -663,7 +708,7 @@ class ViewStringify {
 	/**
 	/**
 	 * Converts passed {@link engine.view.Element Element} to opening tag.
 	 * Converts passed {@link engine.view.Element Element} to opening tag.
 	 * Depending on current configuration opening tag can be simple (`<a>`), contain type prefix (`<container:p>` or
 	 * Depending on current configuration opening tag can be simple (`<a>`), contain type prefix (`<container:p>` or
-	 * `<attribute:a>`), contain priority information ( `<attribute:a priority=20>` ). Element's attributes also
+	 * `<attribute:a>`), contain priority information ( `<attribute:a view-priority="20">` ). Element's attributes also
 	 * will be included (`<a href="http://ckeditor.com" name="foobar">`).
 	 * will be included (`<a href="http://ckeditor.com" name="foobar">`).
 	 *
 	 *
 	 * @private
 	 * @private
@@ -672,17 +717,18 @@ class ViewStringify {
 	 */
 	 */
 	_stringifyElementOpen( element ) {
 	_stringifyElementOpen( element ) {
 		const priority = this._stringifyElementPriority( element );
 		const priority = this._stringifyElementPriority( element );
+
 		const type = this._stringifyElementType( element );
 		const type = this._stringifyElementType( element );
-		const name = [ type, element.name, priority ].filter( i=> i !== '' ).join( ':' );
+		const name = [ type, element.name ].filter( i=> i !== '' ).join( ':' );
 		const attributes = this._stringifyElementAttributes( element );
 		const attributes = this._stringifyElementAttributes( element );
-		const parts = [ name, attributes ];
+		const parts = [ name, priority, attributes ];
 
 
 		return `<${ parts.filter( i => i !== '' ).join( ' ' ) }>`;
 		return `<${ parts.filter( i => i !== '' ).join( ' ' ) }>`;
 	}
 	}
 
 
 	/**
 	/**
 	 * Converts passed {@link engine.view.Element Element} to closing tag.
 	 * Converts passed {@link engine.view.Element Element} to closing tag.
-	 * Depending on current configuration opening tag can be simple (`</a>`) or contain type prefix (`</container:p>` or
+	 * Depending on current configuration closing tag can be simple (`</a>`) or contain type prefix (`</container:p>` or
 	 * `</attribute:a>`).
 	 * `</attribute:a>`).
 	 *
 	 *
 	 * @private
 	 * @private
@@ -690,9 +736,8 @@ class ViewStringify {
 	 * @returns {String}
 	 * @returns {String}
 	 */
 	 */
 	_stringifyElementClose( element ) {
 	_stringifyElementClose( element ) {
-		const priority = this._stringifyElementPriority( element );
 		const type = this._stringifyElementType( element );
 		const type = this._stringifyElementType( element );
-		const name = [ type, element.name, priority ].filter( i=> i !== '' ).join( ':' );
+		const name = [ type, element.name ].filter( i=> i !== '' ).join( ':' );
 
 
 		return `</${ name }>`;
 		return `</${ name }>`;
 	}
 	}
@@ -733,7 +778,7 @@ class ViewStringify {
 	 */
 	 */
 	_stringifyElementPriority( element ) {
 	_stringifyElementPriority( element ) {
 		if ( this.showPriority && element instanceof AttributeElement ) {
 		if ( this.showPriority && element instanceof AttributeElement ) {
-			return element.priority;
+			return `view-priority="${ element.priority }"`;
 		}
 		}
 
 
 		return '';
 		return '';
@@ -786,19 +831,19 @@ function _convertViewElements( rootNode ) {
 
 
 // Converts {@link engine.view.Element Element} to {@link engine.view.AttributeElement AttributeElement} or
 // Converts {@link engine.view.Element Element} to {@link engine.view.AttributeElement AttributeElement} or
 // {@link engine.view.ContainerElement ContainerElement}.
 // {@link engine.view.ContainerElement ContainerElement}.
-// If element's name is in format `attribute:b:1` or `b:11` it will be converted to
+// If element's name is in format `attribute:b` with `view-priority="11"` attribute it will be converted to
 // {@link engine.view.AttributeElement AttributeElement} with priority 11.
 // {@link engine.view.AttributeElement AttributeElement} with priority 11.
 // If element's name is in format `container:p` - it will be converted to
 // If element's name is in format `container:p` - it will be converted to
 // {@link engine.view.ContainerElement ContainerElement}.
 // {@link engine.view.ContainerElement ContainerElement}.
-// If element's name will not contain any additional information - {@link engine.view.Element view Element}will be
+// If element's name will not contain any additional information - {@link engine.view.Element view Element} will be
 // returned.
 // returned.
 //
 //
 // @param {engine.view.Element} viewElement View element to convert.
 // @param {engine.view.Element} viewElement View element to convert.
 // @returns {engine.view.Element|engine.view.AttributeElement|engine.view.ContainerElement} Tree view
 // @returns {engine.view.Element|engine.view.AttributeElement|engine.view.ContainerElement} Tree view
 // element converted according to it's name.
 // element converted according to it's name.
 function _convertElement( viewElement ) {
 function _convertElement( viewElement ) {
-	const info = _convertElementName( viewElement );
 	let newElement;
 	let newElement;
+	const info = _convertElementNameAndPriority( viewElement );
 
 
 	if ( info.type == 'attribute' ) {
 	if ( info.type == 'attribute' ) {
 		newElement = new AttributeElement( info.name );
 		newElement = new AttributeElement( info.name );
@@ -820,24 +865,26 @@ function _convertElement( viewElement ) {
 	return newElement;
 	return newElement;
 }
 }
 
 
-// Converts {@link engine.view.Element#name Element's name} information needed for creating
+// Converts `view-priority` attribute and {@link engine.view.Element#name Element's name} information needed for creating
 // {@link engine.view.AttributeElement AttributeElement} or {@link engine.view.ContainerElement ContainerElement} instance.
 // {@link engine.view.AttributeElement AttributeElement} or {@link engine.view.ContainerElement ContainerElement} instance.
-// Name can be provided in couple formats: as a simple element's name (`div`), as a type and name (`container:div`,
-// `attribute:span`), as a name and priority (`span:12`) and as a type, priority, name trio (`attribute:span:12`);
+// Name can be provided in two formats: as a simple element's name (`div`), or as a type and name (`container:div`,
+// `attribute:span`);
 //
 //
 // @param {engine.view.Element} element Element which name should be converted.
 // @param {engine.view.Element} element Element which name should be converted.
 // @returns {Object} info Object with parsed information.
 // @returns {Object} info Object with parsed information.
 // @returns {String} info.name Parsed name of the element.
 // @returns {String} info.name Parsed name of the element.
 // @returns {String|null} info.type Parsed type of the element, can be `attribute` or `container`.
 // @returns {String|null} info.type Parsed type of the element, can be `attribute` or `container`.
 // returns {Number|null} info.priority Parsed priority of the element.
 // returns {Number|null} info.priority Parsed priority of the element.
-function _convertElementName( viewElement ) {
+function _convertElementNameAndPriority( viewElement ) {
 	const parts = viewElement.name.split( ':' );
 	const parts = viewElement.name.split( ':' );
+	const priority = _convertPriority( viewElement.getAttribute( 'view-priority' ) );
+	viewElement.removeAttribute( 'view-priority' );
 
 
 	if ( parts.length == 1 ) {
 	if ( parts.length == 1 ) {
 		return {
 		return {
 			name: parts[ 0 ],
 			name: parts[ 0 ],
-			type: null,
-			priority: null
+			type: priority !== null ? 'attribute' : null,
+			priority: priority
 		};
 		};
 	}
 	}
 
 
@@ -849,17 +896,6 @@ function _convertElementName( viewElement ) {
 			return {
 			return {
 				name: parts[ 1 ],
 				name: parts[ 1 ],
 				type: type,
 				type: type,
-				priority: null
-			};
-		}
-
-		// Check if name and priority: span:10.
-		const priority = _convertPriority( parts[ 1 ] );
-
-		if ( priority !== null ) {
-			return {
-				name: parts[ 0 ],
-				type: 'attribute',
 				priority: priority
 				priority: priority
 			};
 			};
 		}
 		}
@@ -867,20 +903,6 @@ function _convertElementName( viewElement ) {
 		throw new Error( `Parse error - cannot parse element's name: ${ viewElement.name }.` );
 		throw new Error( `Parse error - cannot parse element's name: ${ viewElement.name }.` );
 	}
 	}
 
 
-	// Check if name is in format type:name:priority.
-	if ( parts.length === 3 ) {
-		const type = _convertType( parts[ 0 ] );
-		const priority = _convertPriority( parts[ 2 ] );
-
-		if ( type && priority !== null ) {
-			return {
-				name: parts[ 1 ],
-				type: type,
-				priority: priority
-			};
-		}
-	}
-
 	throw new Error( `Parse error - cannot parse element's tag name: ${ viewElement.name }.` );
 	throw new Error( `Parse error - cannot parse element's tag name: ${ viewElement.name }.` );
 }
 }
 
 

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

@@ -42,6 +42,8 @@ beforeEach( () => {
 	modelRoot = modelDoc.createRoot();
 	modelRoot = modelDoc.createRoot();
 	modelSelection = modelDoc.selection;
 	modelSelection = modelDoc.selection;
 
 
+	modelDoc.schema.allow( { name: '$text', inside: '$root' } );
+
 	viewDoc = new ViewDocument();
 	viewDoc = new ViewDocument();
 	viewRoot = viewDoc.createRoot( 'div' );
 	viewRoot = viewDoc.createRoot( 'div' );
 	viewSelection = viewDoc.selection;
 	viewSelection = viewDoc.selection;
@@ -87,7 +89,7 @@ describe( 'default converters', () => {
 		it( 'in same container, over attribute', () => {
 		it( 'in same container, over attribute', () => {
 			test(
 			test(
 				[ 1, 5 ],
 				[ 1, 5 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'f{o<strong>ob</strong>a}r'
 				'f{o<strong>ob</strong>a}r'
 			);
 			);
 		} );
 		} );
@@ -95,7 +97,7 @@ describe( 'default converters', () => {
 		it( 'in same container, next to attribute', () => {
 		it( 'in same container, next to attribute', () => {
 			test(
 			test(
 				[ 1, 2 ],
 				[ 1, 2 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'f{o}<strong>ob</strong>ar'
 				'f{o}<strong>ob</strong>ar'
 			);
 			);
 		} );
 		} );
@@ -103,7 +105,7 @@ describe( 'default converters', () => {
 		it( 'in same attribute', () => {
 		it( 'in same attribute', () => {
 			test(
 			test(
 				[ 2, 4 ],
 				[ 2, 4 ],
-				'f<$text bold=true>ooba</$text>r',
+				'f<$text bold="true">ooba</$text>r',
 				'f<strong>o{ob}a</strong>r'
 				'f<strong>o{ob}a</strong>r'
 			);
 			);
 		} );
 		} );
@@ -111,7 +113,7 @@ describe( 'default converters', () => {
 		it( 'in same attribute, selection same as attribute', () => {
 		it( 'in same attribute, selection same as attribute', () => {
 			test(
 			test(
 				[ 2, 4 ],
 				[ 2, 4 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'fo{<strong>ob</strong>}ar'
 				'fo{<strong>ob</strong>}ar'
 			);
 			);
 		} );
 		} );
@@ -119,7 +121,7 @@ describe( 'default converters', () => {
 		it( 'starts in text node, ends in attribute #1', () => {
 		it( 'starts in text node, ends in attribute #1', () => {
 			test(
 			test(
 				[ 1, 3 ],
 				[ 1, 3 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'f{o<strong>o}b</strong>ar'
 				'f{o<strong>o}b</strong>ar'
 			);
 			);
 		} );
 		} );
@@ -127,7 +129,7 @@ describe( 'default converters', () => {
 		it( 'starts in text node, ends in attribute #2', () => {
 		it( 'starts in text node, ends in attribute #2', () => {
 			test(
 			test(
 				[ 1, 4 ],
 				[ 1, 4 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'f{o<strong>ob</strong>}ar'
 				'f{o<strong>ob</strong>}ar'
 			);
 			);
 		} );
 		} );
@@ -135,7 +137,7 @@ describe( 'default converters', () => {
 		it( 'starts in attribute, ends in text node', () => {
 		it( 'starts in attribute, ends in text node', () => {
 			test(
 			test(
 				[ 3, 5 ],
 				[ 3, 5 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'fo<strong>o{b</strong>a}r'
 				'fo<strong>o{b</strong>a}r'
 			);
 			);
 		} );
 		} );
@@ -178,7 +180,7 @@ describe( 'default converters', () => {
 		it( 'in attribute', () => {
 		it( 'in attribute', () => {
 			test(
 			test(
 				[ 3, 3 ],
 				[ 3, 3 ],
-				'f<$text bold=true>ooba</$text>r',
+				'f<$text bold="true">ooba</$text>r',
 				'f<strong>oo{}ba</strong>r'
 				'f<strong>oo{}ba</strong>r'
 			);
 			);
 		} );
 		} );
@@ -195,7 +197,7 @@ describe( 'default converters', () => {
 		it( 'in attribute with extra attributes', () => {
 		it( 'in attribute with extra attributes', () => {
 			test(
 			test(
 				[ 3, 3 ],
 				[ 3, 3 ],
-				'f<$text bold=true>ooba</$text>r',
+				'f<$text bold="true">ooba</$text>r',
 				'f<strong>oo</strong><em><strong>[]</strong></em><strong>ba</strong>r',
 				'f<strong>oo</strong><em><strong>[]</strong></em><strong>ba</strong>r',
 				{ italic: true }
 				{ italic: true }
 			);
 			);
@@ -215,7 +217,7 @@ describe( 'default converters', () => {
 			// Similar test case as above
 			// Similar test case as above
 			test(
 			test(
 				[ 3, 3 ],
 				[ 3, 3 ],
-				'f<$text bold=true>ooba</$text>r',
+				'f<$text bold="true">ooba</$text>r',
 				'f<strong>ooba</strong>r' // No selection in view.
 				'f<strong>ooba</strong>r' // No selection in view.
 			);
 			);
 		} );
 		} );
@@ -311,16 +313,16 @@ describe( 'clean-up', () => {
 
 
 describe( 'using element creator for attributes conversion', () => {
 describe( 'using element creator for attributes conversion', () => {
 	beforeEach( () => {
 	beforeEach( () => {
-		function styleElementCreator( styleValue ) {
-			if ( styleValue == 'important' ) {
+		function themeElementCreator( themeValue ) {
+			if ( themeValue == 'important' ) {
 				return new ViewAttributeElement( 'strong', { style: 'text-transform:uppercase;' } );
 				return new ViewAttributeElement( 'strong', { style: 'text-transform:uppercase;' } );
-			} else if ( styleValue == 'gold' ) {
+			} else if ( themeValue == 'gold' ) {
 				return new ViewAttributeElement( 'span', { style: 'color:yellow;' } );
 				return new ViewAttributeElement( 'span', { style: 'color:yellow;' } );
 			}
 			}
 		}
 		}
 
 
-		dispatcher.on( 'selectionAttribute:style', convertSelectionAttribute( styleElementCreator ) );
-		dispatcher.on( 'addAttribute:style', wrap( styleElementCreator ) );
+		dispatcher.on( 'selectionAttribute:theme', convertSelectionAttribute( themeElementCreator ) );
+		dispatcher.on( 'addAttribute:theme', wrap( themeElementCreator ) );
 
 
 		dispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
 		dispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
 	} );
 	} );
@@ -329,7 +331,7 @@ describe( 'using element creator for attributes conversion', () => {
 		it( 'in same container, over attribute', () => {
 		it( 'in same container, over attribute', () => {
 			test(
 			test(
 				[ 1, 5 ],
 				[ 1, 5 ],
-				'fo<$text style="gold">ob</$text>ar',
+				'fo<$text theme="gold">ob</$text>ar',
 				'f{o<span style="color:yellow;">ob</span>a}r'
 				'f{o<span style="color:yellow;">ob</span>a}r'
 			);
 			);
 		} );
 		} );
@@ -337,7 +339,7 @@ describe( 'using element creator for attributes conversion', () => {
 		it( 'in same attribute', () => {
 		it( 'in same attribute', () => {
 			test(
 			test(
 				[ 2, 4 ],
 				[ 2, 4 ],
-				'f<$text style="gold">ooba</$text>r',
+				'f<$text theme="gold">ooba</$text>r',
 				'f<span style="color:yellow;">o{ob}a</span>r'
 				'f<span style="color:yellow;">o{ob}a</span>r'
 			);
 			);
 		} );
 		} );
@@ -345,7 +347,7 @@ describe( 'using element creator for attributes conversion', () => {
 		it( 'in same attribute, selection same as attribute', () => {
 		it( 'in same attribute, selection same as attribute', () => {
 			test(
 			test(
 				[ 2, 4 ],
 				[ 2, 4 ],
-				'fo<$text style="important">ob</$text>ar',
+				'fo<$text theme="important">ob</$text>ar',
 				'fo{<strong style="text-transform:uppercase;">ob</strong>}ar'
 				'fo{<strong style="text-transform:uppercase;">ob</strong>}ar'
 			);
 			);
 		} );
 		} );
@@ -353,7 +355,7 @@ describe( 'using element creator for attributes conversion', () => {
 		it( 'starts in attribute, ends in text node', () => {
 		it( 'starts in attribute, ends in text node', () => {
 			test(
 			test(
 				[ 3, 5 ],
 				[ 3, 5 ],
-				'fo<$text style="important">ob</$text>ar',
+				'fo<$text theme="important">ob</$text>ar',
 				'fo<strong style="text-transform:uppercase;">o{b</strong>a}r'
 				'fo<strong style="text-transform:uppercase;">o{b</strong>a}r'
 			);
 			);
 		} );
 		} );
@@ -363,24 +365,24 @@ describe( 'using element creator for attributes conversion', () => {
 		it( 'in attribute', () => {
 		it( 'in attribute', () => {
 			test(
 			test(
 				[ 3, 3 ],
 				[ 3, 3 ],
-				'f<$text style="gold">ooba</$text>r',
+				'f<$text theme="gold">ooba</$text>r',
 				'f<span style="color:yellow;">oo{}ba</span>r'
 				'f<span style="color:yellow;">oo{}ba</span>r'
 			);
 			);
 		} );
 		} );
 
 
-		it( 'in container with style attribute', () => {
+		it( 'in container with theme attribute', () => {
 			test(
 			test(
 				[ 1, 1 ],
 				[ 1, 1 ],
 				'foobar',
 				'foobar',
 				'f<strong style="text-transform:uppercase;">[]</strong>oobar',
 				'f<strong style="text-transform:uppercase;">[]</strong>oobar',
-				{ style: 'important' }
+				{ theme: 'important' }
 			);
 			);
 		} );
 		} );
 
 
-		it( 'in style attribute with extra attributes #1', () => {
+		it( 'in theme attribute with extra attributes #1', () => {
 			test(
 			test(
 				[ 3, 3 ],
 				[ 3, 3 ],
-				'f<$text style="gold">ooba</$text>r',
+				'f<$text theme="gold">ooba</$text>r',
 				'f<span style="color:yellow;">oo</span>' +
 				'f<span style="color:yellow;">oo</span>' +
 				'<em><span style="color:yellow;">[]</span></em>' +
 				'<em><span style="color:yellow;">[]</span></em>' +
 				'<span style="color:yellow;">ba</span>r',
 				'<span style="color:yellow;">ba</span>r',
@@ -388,18 +390,18 @@ describe( 'using element creator for attributes conversion', () => {
 			);
 			);
 		} );
 		} );
 
 
-		it( 'in style attribute with extra attributes #2', () => {
+		it( 'in theme attribute with extra attributes #2', () => {
 			// In contrary to test above, we don't have strong + span on the selection.
 			// In contrary to test above, we don't have strong + span on the selection.
 			// This is because strong and span are both created by the same attribute.
 			// This is because strong and span are both created by the same attribute.
 			// Since style="important" overwrites style="gold" on selection, we have only strong element.
 			// Since style="important" overwrites style="gold" on selection, we have only strong element.
 			// In example above, selection has both style and italic attribute.
 			// In example above, selection has both style and italic attribute.
 			test(
 			test(
 				[ 3, 3 ],
 				[ 3, 3 ],
-				'f<$text style="gold">ooba</$text>r',
+				'f<$text theme="gold">ooba</$text>r',
 				'f<span style="color:yellow;">oo</span>' +
 				'f<span style="color:yellow;">oo</span>' +
 				'<strong style="text-transform:uppercase;">[]</strong>' +
 				'<strong style="text-transform:uppercase;">[]</strong>' +
 				'<span style="color:yellow;">ba</span>r',
 				'<span style="color:yellow;">ba</span>r',
-				{ style: 'important' }
+				{ theme: 'important' }
 			);
 			);
 		} );
 		} );
 	} );
 	} );
@@ -407,6 +409,10 @@ describe( 'using element creator for attributes conversion', () => {
 
 
 describe( 'table cell selection converter', () => {
 describe( 'table cell selection converter', () => {
 	beforeEach( () => {
 	beforeEach( () => {
+		modelDoc.schema.registerItem( 'table', '$block' );
+		modelDoc.schema.registerItem( 'tr', '$block' );
+		modelDoc.schema.registerItem( 'td', '$block' );
+
 		// "Universal" converter to convert table structure.
 		// "Universal" converter to convert table structure.
 		const tableConverter = insertElement( ( data ) => new ViewContainerElement( data.item.name ) );
 		const tableConverter = insertElement( ( data ) => new ViewContainerElement( data.item.name ) );
 		dispatcher.on( 'insert:table', tableConverter );
 		dispatcher.on( 'insert:table', tableConverter );
@@ -437,13 +443,12 @@ describe( 'table cell selection converter', () => {
 	it( 'should not be used to convert selection that is not on table cell', () => {
 	it( 'should not be used to convert selection that is not on table cell', () => {
 		test(
 		test(
 			[ 1, 5 ],
 			[ 1, 5 ],
-			'f<selection>o<$text bold=true>ob</$text>a</selection>r',
+			'f{o<$text bold="true">ob</$text>a}r',
 			'f{o<strong>ob</strong>a}r'
 			'f{o<strong>ob</strong>a}r'
 		);
 		);
 	} );
 	} );
 
 
 	it( 'should add a class to the selected table cell', () => {
 	it( 'should add a class to the selected table cell', () => {
-		modelDoc.schema.registerItem( 'table', '$block' );
 		test(
 		test(
 			// table tr#0 |td#0, table tr#0 td#0|
 			// table tr#0 |td#0, table tr#0 td#0|
 			[ [ 0, 0, 0 ], [ 0, 0, 1 ] ],
 			[ [ 0, 0, 0 ], [ 0, 0, 1 ] ],
@@ -453,7 +458,6 @@ describe( 'table cell selection converter', () => {
 	} );
 	} );
 
 
 	it( 'should not be used if selection contains more than just a table cell', () => {
 	it( 'should not be used if selection contains more than just a table cell', () => {
-		modelDoc.schema.registerItem( 'table', '$block' );
 		test(
 		test(
 			// table tr td#1, table tr#2
 			// table tr td#1, table tr#2
 			[ [ 0, 0, 0, 1 ], [ 0, 0, 2 ] ],
 			[ [ 0, 0, 0, 1 ], [ 0, 0, 2 ] ],

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

@@ -47,7 +47,8 @@ describe( 'convertSelectionChange', () => {
 
 
 		convertSelection( null, { newSelection: viewSelection } );
 		convertSelection( null, { newSelection: viewSelection } );
 
 
-		expect( modelGetData( model ) ).to.equal( '<paragraph>f<selection />oo</paragraph><paragraph>bar</paragraph>' );
+		expect( modelGetData( model ) ).to.equals( '<paragraph>f[]oo</paragraph><paragraph>bar</paragraph>' );
+		expect( modelGetData( model ) ).to.equal( '<paragraph>f[]oo</paragraph><paragraph>bar</paragraph>' );
 	} );
 	} );
 
 
 	it( 'should support unicode', () => {
 	it( 'should support unicode', () => {
@@ -64,7 +65,7 @@ describe( 'convertSelectionChange', () => {
 
 
 		convertSelection( null, { newSelection: viewSelection } );
 		convertSelection( null, { newSelection: viewSelection } );
 
 
-		expect( modelGetData( model ) ).to.equal( '<paragraph>நி<selection>லைக்</selection>கு</paragraph>' );
+		expect( modelGetData( model ) ).to.equal( '<paragraph>நி[லைக்]கு</paragraph>' );
 	} );
 	} );
 
 
 	it( 'should convert multi ranges selection', () => {
 	it( 'should convert multi ranges selection', () => {
@@ -76,9 +77,8 @@ describe( 'convertSelectionChange', () => {
 
 
 		convertSelection( null, { newSelection: viewSelection } );
 		convertSelection( null, { newSelection: viewSelection } );
 
 
-		// Too bad getData shows only the first range.
 		expect( modelGetData( model ) ).to.equal(
 		expect( modelGetData( model ) ).to.equal(
-			'<paragraph>f<selection>o</selection>o</paragraph><paragraph>bar</paragraph>' );
+			'<paragraph>f[o]o</paragraph><paragraph>b[a]r</paragraph>' );
 
 
 		const ranges = Array.from( model.selection.getRanges() );
 		const ranges = Array.from( model.selection.getRanges() );
 		expect( ranges.length ).to.equal( 2 );
 		expect( ranges.length ).to.equal( 2 );
@@ -98,11 +98,12 @@ describe( 'convertSelectionChange', () => {
 		const viewSelection = new ViewSelection();
 		const viewSelection = new ViewSelection();
 		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
 		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
 			viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), true );
 			viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), true );
+		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
+			viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ), true );
 
 
 		convertSelection( null, { newSelection: viewSelection } );
 		convertSelection( null, { newSelection: viewSelection } );
 
 
-		// Too bad getData shows only the first range.
-		expect( modelGetData( model ) ).to.equal(
-			'<paragraph>f<selection backward>o</selection>o</paragraph><paragraph>bar</paragraph>' );
+		expect( modelGetData( model ) ).to.equal( '<paragraph>f[o]o</paragraph><paragraph>b[a]r</paragraph>' );
+		expect( model.selection.isBackward ).to.true;
 	} );
 	} );
 } );
 } );

+ 6 - 3
packages/ckeditor5-engine/tests/datacontroller.js

@@ -78,7 +78,7 @@ describe( 'DataController', () => {
 			const model = data.parse( '<p>foo<b>bar</b></p>' );
 			const model = data.parse( '<p>foo<b>bar</b></p>' );
 
 
 			expect( stringify( model ) ).to.equal(
 			expect( stringify( model ) ).to.equal(
-				'<paragraph>foo<$text bold=true>bar</$text></paragraph>' );
+				'<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 		} );
 		} );
 	} );
 	} );
 
 
@@ -163,6 +163,7 @@ describe( 'DataController', () => {
 		} );
 		} );
 
 
 		it( 'should get text directly in root', () => {
 		it( 'should get text directly in root', () => {
+			modelDocument.schema.allow( { name: '$text', inside: '$root' } );
 			setData( modelDocument, 'foo' );
 			setData( modelDocument, 'foo' );
 
 
 			expect( data.get() ).to.equal( 'foo' );
 			expect( data.get() ).to.equal( 'foo' );
@@ -170,7 +171,7 @@ describe( 'DataController', () => {
 
 
 		it( 'should get paragraphs without bold', () => {
 		it( 'should get paragraphs without bold', () => {
 			modelDocument.schema.registerItem( 'paragraph', '$block' );
 			modelDocument.schema.registerItem( 'paragraph', '$block' );
-			setData( modelDocument, '<paragraph>foo<$text bold=true>bar</$text></paragraph>' );
+			setData( modelDocument, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 
 
@@ -179,7 +180,7 @@ describe( 'DataController', () => {
 
 
 		it( 'should get paragraphs with bold', () => {
 		it( 'should get paragraphs with bold', () => {
 			modelDocument.schema.registerItem( 'paragraph', '$block' );
 			modelDocument.schema.registerItem( 'paragraph', '$block' );
-			setData( modelDocument, '<paragraph>foo<$text bold=true>bar</$text></paragraph>' );
+			setData( modelDocument, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
 
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 			buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' );
 			buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
 			buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
@@ -189,6 +190,8 @@ describe( 'DataController', () => {
 
 
 		it( 'should get root name as a parameter', () => {
 		it( 'should get root name as a parameter', () => {
 			modelDocument.schema.registerItem( 'paragraph', '$block' );
 			modelDocument.schema.registerItem( 'paragraph', '$block' );
+			modelDocument.schema.allow( { name: '$text', inside: '$root' } );
+
 			setData( modelDocument, '<paragraph>foo</paragraph>', { rootName: 'main' } );
 			setData( modelDocument, '<paragraph>foo</paragraph>', { rootName: 'main' } );
 			setData( modelDocument, 'Bar', { rootName: 'title' } );
 			setData( modelDocument, 'Bar', { rootName: 'title' } );
 
 

+ 103 - 0
packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js

@@ -0,0 +1,103 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: browser-only */
+
+import XmlDataProcessor from '/ckeditor5/engine/dataprocessor/xmldataprocessor.js';
+import xssTemplates from '/tests/engine/dataprocessor/_utils/xsstemplates.js';
+import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
+
+describe( 'XmlDataProcessor', () => {
+	let dataProcessor;
+
+	beforeEach( () => {
+		dataProcessor = new XmlDataProcessor();
+	} );
+
+	describe( 'toView', () => {
+		it( 'should return empty DocumentFragment when empty string is passed', () => {
+			const fragment = dataProcessor.toView( '' );
+			expect( fragment ).to.be.an.instanceOf( ViewDocumentFragment );
+			expect( fragment.childCount ).to.equal( 0 );
+		} );
+
+		it( 'should convert XML to DocumentFragment with single text node', () => {
+			const fragment = dataProcessor.toView( 'foo bar' );
+
+			expect( stringify( fragment ) ).to.equal( 'foo bar' );
+		} );
+
+		it( 'should convert HTML to DocumentFragment with multiple child nodes', () => {
+			const fragment = dataProcessor.toView( '<p>foo</p><p>bar</p>' );
+
+			expect( stringify( fragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
+		} );
+
+		it( 'should not add any additional nodes', () => {
+			const fragment = dataProcessor.toView( 'foo <b>bar</b> text' );
+
+			expect( stringify( fragment ) ).to.equal( 'foo <b>bar</b> text' );
+		} );
+
+		it( 'should allow to use registered namespaces', () => {
+			dataProcessor = new XmlDataProcessor( {
+				namespaces: [ 'foo', 'bar' ]
+			} );
+
+			const fragment = dataProcessor.toView( '<foo:a><bar:b></bar:b></foo:a><bar:b><foo:a></foo:a></bar:b>' );
+
+			expect( stringify( fragment ) ).to.equal( '<foo:a><bar:b></bar:b></foo:a><bar:b><foo:a></foo:a></bar:b>' );
+		} );
+
+		it( 'should throw an error when use not registered namespaces', () => {
+			expect( () => {
+				dataProcessor.toView( '<foo:a></foo:a>' );
+			} ).to.throw( Error, /Parse error/ );
+		} );
+
+		it( 'should thrown an error when markup is invalid', () => {
+			expect( () => {
+				dataProcessor.toView( '<b>missing closing tag' );
+			} ).to.throw( Error, /Parse error/ );
+		} );
+
+		// Test against XSS attacks.
+		for ( let name in xssTemplates ) {
+			const input = xssTemplates[ name ].replace( /%xss%/g, 'testXss()' );
+
+			it( 'should prevent XSS attacks: ' + name, ( done ) => {
+				window.testXss = sinon.spy();
+				dataProcessor.toView( input );
+
+				setTimeout( () => {
+					sinon.assert.notCalled( window.testXss );
+					done();
+				}, 10 );
+			} );
+		}
+	} );
+
+	describe( 'toData', () => {
+		it( 'should return empty string when empty DocumentFragment is passed', () => {
+			const fragment = new ViewDocumentFragment();
+
+			expect( dataProcessor.toData( fragment ) ).to.equal( '' );
+		} );
+
+		it( 'should return text if document fragment with single text node is passed', () => {
+			const fragment = new ViewDocumentFragment();
+			fragment.appendChildren( parse( 'foo bar' ) );
+
+			expect( dataProcessor.toData( fragment ) ).to.equal( 'foo bar' );
+		} );
+
+		it( 'should convert HTML to DocumentFragment with multiple child nodes', () => {
+			const fragment = parse( '<p>foo</p><p>bar</p>' );
+
+			expect( dataProcessor.toData( fragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
+		} );
+	} );
+} );

+ 4 - 3
packages/ckeditor5-engine/tests/editingcontroller.js

@@ -129,7 +129,8 @@ describe( 'EditingController', () => {
 			const modelData = new ModelDocumentFragment( parse(
 			const modelData = new ModelDocumentFragment( parse(
 				'<paragraph>foo</paragraph>' +
 				'<paragraph>foo</paragraph>' +
 				'<paragraph></paragraph>' +
 				'<paragraph></paragraph>' +
-				'<paragraph>bar</paragraph>'
+				'<paragraph>bar</paragraph>',
+				model.schema
 			)._children );
 			)._children );
 
 
 			model.enqueueChanges( () => {
 			model.enqueueChanges( () => {
@@ -175,7 +176,7 @@ describe( 'EditingController', () => {
 					expect( getModelData( model ) ).to.equal(
 					expect( getModelData( model ) ).to.equal(
 						'<paragraph>foo</paragraph>' +
 						'<paragraph>foo</paragraph>' +
 						'<paragraph></paragraph>' +
 						'<paragraph></paragraph>' +
-						'<paragraph>b<selection>a</selection>r</paragraph>' );
+						'<paragraph>b[a]r</paragraph>' );
 					done();
 					done();
 				} );
 				} );
 			} );
 			} );
@@ -247,7 +248,7 @@ describe( 'EditingController', () => {
 			editing.destroy();
 			editing.destroy();
 
 
 			model.enqueueChanges( () => {
 			model.enqueueChanges( () => {
-				const modelData = parse( '<paragraph>foo</paragraph>' ).getChild( 0 );
+				const modelData = parse( '<paragraph>foo</paragraph>', model.schema ).getChild( 0 );
 				model.batch().insert( ModelPosition.createAt( model.getRoot(), 0 ), modelData );
 				model.batch().insert( ModelPosition.createAt( model.getRoot(), 0 ), modelData );
 			} );
 			} );
 
 

+ 7 - 6
packages/ckeditor5-engine/tests/model/composer/composer.js

@@ -22,18 +22,18 @@ describe( 'Composer', () => {
 
 
 	describe( 'constructor', () => {
 	describe( 'constructor', () => {
 		it( 'attaches deleteContents default listener', () => {
 		it( 'attaches deleteContents default listener', () => {
-			setData( document, '<p>f<selection>oo</p><p>ba</selection>r</p>' );
+			setData( document, '<p>f[oo</p><p>ba]r</p>' );
 
 
 			const batch = document.batch();
 			const batch = document.batch();
 
 
 			composer.fire( 'deleteContents', { batch, selection: document.selection } );
 			composer.fire( 'deleteContents', { batch, selection: document.selection } );
 
 
-			expect( getData( document ) ).to.equal( '<p>f<selection /></p><p>r</p>' );
+			expect( getData( document ) ).to.equal( '<p>f[]</p><p>r</p>' );
 			expect( batch.deltas ).to.not.be.empty;
 			expect( batch.deltas ).to.not.be.empty;
 		} );
 		} );
 
 
 		it( 'attaches deleteContents default listener which passes options', () => {
 		it( 'attaches deleteContents default listener which passes options', () => {
-			setData( document, '<p>f<selection>oo</p><p>ba</selection>r</p>' );
+			setData( document, '<p>f[oo</p><p>ba]r</p>' );
 
 
 			const batch = document.batch();
 			const batch = document.batch();
 
 
@@ -43,11 +43,11 @@ describe( 'Composer', () => {
 				options: { merge: true }
 				options: { merge: true }
 			} );
 			} );
 
 
-			expect( getData( document ) ).to.equal( '<p>f<selection />r</p>' );
+			expect( getData( document ) ).to.equal( '<p>f[]r</p>' );
 		} );
 		} );
 
 
 		it( 'attaches modifySelection default listener', () => {
 		it( 'attaches modifySelection default listener', () => {
-			setData( document, '<p>foo<selection />bar</p>' );
+			setData( document, '<p>foo[]bar</p>' );
 
 
 			composer.fire( 'modifySelection', {
 			composer.fire( 'modifySelection', {
 				selection: document.selection,
 				selection: document.selection,
@@ -57,7 +57,8 @@ describe( 'Composer', () => {
 			} );
 			} );
 
 
 			expect( getData( document ) )
 			expect( getData( document ) )
-				.to.equal( '<p>fo<selection backward>o</selection>bar</p>' );
+				.to.equal( '<p>fo[o]bar</p>' );
+			expect( document.selection.isBackward ).to.true;
 		} );
 		} );
 	} );
 	} );
 
 

+ 96 - 71
packages/ckeditor5-engine/tests/model/composer/deletecontents.js

@@ -26,6 +26,8 @@ describe( 'Delete utils', () => {
 		schema.registerItem( 'pchild' );
 		schema.registerItem( 'pchild' );
 
 
 		schema.allow( { name: 'pchild', inside: 'p' } );
 		schema.allow( { name: 'pchild', inside: 'p' } );
+		schema.allow( { name: '$text', inside: '$root' } );
+		schema.allow( { name: 'img', inside: '$root' } );
 		schema.allow( { name: '$text', attributes: [ 'bold', 'italic' ] } );
 		schema.allow( { name: '$text', attributes: [ 'bold', 'italic' ] } );
 		schema.allow( { name: 'p', attributes: [ 'align' ] } );
 		schema.allow( { name: 'p', attributes: [ 'align' ] } );
 	} );
 	} );
@@ -34,78 +36,100 @@ describe( 'Delete utils', () => {
 		describe( 'in simple scenarios', () => {
 		describe( 'in simple scenarios', () => {
 			test(
 			test(
 				'does nothing on collapsed selection',
 				'does nothing on collapsed selection',
-				'f<selection />oo',
-				'f<selection />oo'
+				'f[]oo',
+				'f[]oo'
 			);
 			);
 
 
 			test(
 			test(
 				'deletes single character',
 				'deletes single character',
-				'f<selection>o</selection>o',
-				'f<selection />o'
+				'f[o]o',
+				'f[]o'
 			);
 			);
 
 
-			test(
-				'xdeletes single character (backward selection)',
-				'f<selection backward>o</selection>o',
-				'f<selection />o'
-			);
+			it( 'deletes single character (backward selection)' , () => {
+				setData( document, 'f[o]o', { lastRangeBackward: true } );
+
+				deleteContents( document.batch(), document.selection );
+
+				expect( getData( document ) ).to.equal( 'f[]o' );
+			} );
 
 
 			test(
 			test(
 				'deletes whole text',
 				'deletes whole text',
-				'<selection>foo</selection>',
-				'<selection />'
+				'[foo]',
+				'[]'
 			);
 			);
 
 
 			test(
 			test(
 				'deletes whole text between nodes',
 				'deletes whole text between nodes',
-				'<img></img><selection>foo</selection><img></img>',
-				'<img></img><selection /><img></img>'
+				'<img></img>[foo]<img></img>',
+				'<img></img>[]<img></img>'
 			);
 			);
 
 
 			test(
 			test(
 				'deletes an element',
 				'deletes an element',
-				'x<selection><img></img></selection>y',
-				'x<selection />y'
+				'x[<img></img>]y',
+				'x[]y'
 			);
 			);
 
 
 			test(
 			test(
 				'deletes a bunch of nodes',
 				'deletes a bunch of nodes',
-				'w<selection>x<img></img>y</selection>z',
-				'w<selection />z'
+				'w[x<img></img>y]z',
+				'w[]z'
 			);
 			);
 
 
 			test(
 			test(
 				'does not break things when option.merge passed',
 				'does not break things when option.merge passed',
-				'w<selection>x<img></img>y</selection>z',
-				'w<selection />z',
+				'w[x<img></img>y]z',
+				'w[]z',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 		} );
 		} );
 
 
 		describe( 'with text attributes', () => {
 		describe( 'with text attributes', () => {
-			test(
-				'deletes characters (first half has attrs)',
-				'<$text bold=true>fo<selection bold=true>o</$text>b</selection>ar',
-				'<$text bold=true>fo</$text><selection bold=true />ar'
-			);
+			it( 'deletes characters (first half has attrs)', () => {
+				setData( document, '<$text bold="true">fo[o</$text>b]ar', { selectionAttributes: {
+					bold: true
+				} } );
 
 
-			test(
-				'deletes characters (2nd half has attrs)',
-				'fo<selection bold=true>o<$text bold=true>b</selection>ar</$text>',
-				'fo<selection /><$text bold=true>ar</$text>'
-			);
+				deleteContents( document.batch(), document.selection );
 
 
-			test(
-				'clears selection attrs when emptied content',
-				'<p>x</p><p><selection bold=true><$text bold=true>foo</$text></selection></p><p>y</p>',
-				'<p>x</p><p><selection /></p><p>y</p>'
-			);
+				expect( getData( document ) ).to.equal( '<$text bold="true">fo[]</$text>ar' );
+				expect( document.selection.getAttribute( 'bold' ) ).to.equal( 'true' );
+			} );
 
 
-			test(
-				'leaves selection attributes when text contains them',
-				'<p>x<$text bold=true>a<selection bold=true>foo</selection>b</$text>y</p>',
-				'<p>x<$text bold=true>a<selection bold=true />b</$text>y</p>'
-			);
+			it( 'deletes characters (2nd half has attrs)', () => {
+				setData( document, 'fo[o<$text bold="true">b]ar</$text>', { selectionAttributes: {
+					bold: true
+				} } );
+
+				deleteContents( document.batch(), document.selection );
+
+				expect( getData( document ) ).to.equal( 'fo[]<$text bold="true">ar</$text>' );
+				expect( document.selection.getAttribute( 'bold' ) ).to.undefined;
+			} );
+
+			it( 'clears selection attrs when emptied content', () => {
+				setData( document, '<p>x</p><p>[<$text bold="true">foo</$text>]</p><p>y</p>', { selectionAttributes: {
+					bold: true
+				} } );
+
+				deleteContents( document.batch(), document.selection );
+
+				expect( getData( document ) ).to.equal( '<p>x</p><p>[]</p><p>y</p>' );
+				expect( document.selection.getAttribute( 'bold' ) ).to.undefined;
+			} );
+
+			it( 'leaves selection attributes when text contains them', () => {
+				setData( document, '<p>x<$text bold="true">a[foo]b</$text>y</p>', { selectionAttributes: {
+					bold: true
+				} } );
+
+				deleteContents( document.batch(), document.selection );
+
+				expect( getData( document ) ).to.equal( '<p>x<$text bold="true">a[]b</$text>y</p>' );
+				expect( document.selection.getAttribute( 'bold' ) ).to.equal( 'true' );
+			} );
 		} );
 		} );
 
 
 		// Note: The algorithm does not care what kind of it's merging as it knows nothing useful about these elements.
 		// Note: The algorithm does not care what kind of it's merging as it knows nothing useful about these elements.
@@ -120,101 +144,102 @@ describe( 'Delete utils', () => {
 		describe( 'in multi-element scenarios', () => {
 		describe( 'in multi-element scenarios', () => {
 			test(
 			test(
 				'do not merge when no need to',
 				'do not merge when no need to',
-				'<p>x</p><p><selection>foo</selection></p><p>y</p>',
-				'<p>x</p><p><selection /></p><p>y</p>',
+				'<p>x</p><p>[foo]</p><p>y</p>',
+				'<p>x</p><p>[]</p><p>y</p>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'merges second element into the first one (same name)',
 				'merges second element into the first one (same name)',
-				'<p>x</p><p>fo<selection>o</p><p>b</selection>ar</p><p>y</p>',
-				'<p>x</p><p>fo<selection />ar</p><p>y</p>',
+				'<p>x</p><p>fo[o</p><p>b]ar</p><p>y</p>',
+				'<p>x</p><p>fo[]ar</p><p>y</p>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'does not merge second element into the first one (same name, !option.merge)',
 				'does not merge second element into the first one (same name, !option.merge)',
-				'<p>x</p><p>fo<selection>o</p><p>b</selection>ar</p><p>y</p>',
-				'<p>x</p><p>fo<selection /></p><p>ar</p><p>y</p>'
+				'<p>x</p><p>fo[o</p><p>b]ar</p><p>y</p>',
+				'<p>x</p><p>fo[]</p><p>ar</p><p>y</p>'
 			);
 			);
 
 
 			test(
 			test(
 				'merges second element into the first one (same name)',
 				'merges second element into the first one (same name)',
-				'<p>x</p><p>fo<selection>o</p><p>b</selection>ar</p><p>y</p>',
-				'<p>x</p><p>fo<selection />ar</p><p>y</p>',
+				'<p>x</p><p>fo[o</p><p>b]ar</p><p>y</p>',
+				'<p>x</p><p>fo[]ar</p><p>y</p>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'merges second element into the first one (different name)',
 				'merges second element into the first one (different name)',
-				'<p>x</p><h1>fo<selection>o</h1><p>b</selection>ar</p><p>y</p>',
-				'<p>x</p><h1>fo<selection />ar</h1><p>y</p>',
+				'<p>x</p><h1>fo[o</h1><p>b]ar</p><p>y</p>',
+				'<p>x</p><h1>fo[]ar</h1><p>y</p>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 
 
-			test(
-				'merges second element into the first one (different name, backward selection)',
-				'<p>x</p><h1>fo<selection backward>o</h1><p>b</selection>ar</p><p>y</p>',
-				'<p>x</p><h1>fo<selection />ar</h1><p>y</p>',
-				{ merge: true }
-			);
+			it( 'merges second element into the first one (different name, backward selection)', () => {
+				setData( document, '<p>x</p><h1>fo[o</h1><p>b]ar</p><p>y</p>', { lastRangeBackward: true } );
+
+				deleteContents( document.batch(), document.selection, { merge: true } );
+
+				expect( getData( document ) ).to.equal( '<p>x</p><h1>fo[]ar</h1><p>y</p>' );
+			} );
 
 
 			test(
 			test(
 				'merges second element into the first one (different attrs)',
 				'merges second element into the first one (different attrs)',
-				'<p>x</p><p align="l">fo<selection>o</p><p>b</selection>ar</p><p>y</p>',
-				'<p>x</p><p align="l">fo<selection />ar</p><p>y</p>',
+				'<p>x</p><p align="l">fo[o</p><p>b]ar</p><p>y</p>',
+				'<p>x</p><p align="l">fo[]ar</p><p>y</p>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'merges second element to an empty first element',
 				'merges second element to an empty first element',
-				'<p>x</p><h1><selection></h1><p>fo</selection>o</p><p>y</p>',
-				'<p>x</p><h1><selection />o</h1><p>y</p>',
+				'<p>x</p><h1>[</h1><p>fo]o</p><p>y</p>',
+				'<p>x</p><h1>[]o</h1><p>y</p>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'merges elements when deep nested',
 				'merges elements when deep nested',
-				'<p>x<pchild>fo<selection>o</pchild></p><p><pchild>b</selection>ar</pchild>y</p>',
-				'<p>x<pchild>fo<selection />ar</pchild>y</p>',
+				'<p>x<pchild>fo[o</pchild></p><p><pchild>b]ar</pchild>y</p>',
+				'<p>x<pchild>fo[]ar</pchild>y</p>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 
 
 			// For code coverage reasons.
 			// For code coverage reasons.
 			test(
 			test(
 				'merges element when selection is in two consecutive nodes even when it is empty',
 				'merges element when selection is in two consecutive nodes even when it is empty',
-				'<p>foo<selection></p><p></selection>bar</p>',
-				'<p>foo<selection />bar</p>',
+				'<p>foo[</p><p>]bar</p>',
+				'<p>foo[]bar</p>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 
 
 			// If you disagree with this case please read the notes before this section.
 			// If you disagree with this case please read the notes before this section.
 			test(
 			test(
 				'merges elements when left end deep nested',
 				'merges elements when left end deep nested',
-				'<p>x<pchild>fo<selection>o</pchild></p><p>b</selection>ary</p>',
-				'<p>x<pchild>fo<selection /></pchild>ary</p>',
+				'<p>x<pchild>fo[o</pchild></p><p>b]ary</p>',
+				'<p>x<pchild>fo[]</pchild>ary</p>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 
 
 			// If you disagree with this case please read the notes before this section.
 			// If you disagree with this case please read the notes before this section.
 			test(
 			test(
 				'merges elements when right end deep nested',
 				'merges elements when right end deep nested',
-				'<p>xfo<selection>o</p><p><pchild>b</selection>ar</pchild>y<img></img></p>',
-				'<p>xfo<selection /><pchild>ar</pchild>y<img></img></p>',
+				'<p>xfo[o</p><p><pchild>b]ar</pchild>y<img></img></p>',
+				'<p>xfo[]<pchild>ar</pchild>y<img></img></p>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'merges elements when more content in the right branch',
 				'merges elements when more content in the right branch',
-				'<p>xfo<selection>o</p><p>b</selection>a<pchild>r</pchild>y</p>',
-				'<p>xfo<selection />a<pchild>r</pchild>y</p>',
+				'<p>xfo[o</p><p>b]a<pchild>r</pchild>y</p>',
+				'<p>xfo[]a<pchild>r</pchild>y</p>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'leaves just one element when all selected',
 				'leaves just one element when all selected',
-				'<h1><selection>x</h1><p>foo</p><p>y</selection></p>',
-				'<h1><selection /></h1>',
+				'<h1>[x</h1><p>foo</p><p>y]</p>',
+				'<h1>[]</h1>',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 		} );
 		} );

+ 212 - 161
packages/ckeditor5-engine/tests/model/composer/modifyselection.js

@@ -16,6 +16,9 @@ describe( 'Delete utils', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		document = new Document();
 		document = new Document();
 		document.schema.registerItem( 'p', '$block' );
 		document.schema.registerItem( 'p', '$block' );
+		document.schema.registerItem( 'x', '$block' );
+		document.schema.registerItem( 'img', '$inline' );
+		document.schema.allow( { name: '$text', inside: '$root' } );
 		document.createRoot();
 		document.createRoot();
 	} );
 	} );
 
 
@@ -24,309 +27,357 @@ describe( 'Delete utils', () => {
 			describe( 'within element', () => {
 			describe( 'within element', () => {
 				test(
 				test(
 					'does nothing on empty content',
 					'does nothing on empty content',
-					'<selection />',
-					'<selection />'
+					'[]',
+					'[]'
 				);
 				);
 
 
 				test(
 				test(
 					'does nothing on empty content (with empty element)',
 					'does nothing on empty content (with empty element)',
-					'<p><selection /></p>',
-					'<p><selection /></p>'
+					'<p>[]</p>',
+					'<p>[]</p>'
 				);
 				);
 
 
 				test(
 				test(
 					'does nothing on empty content (backward)',
 					'does nothing on empty content (backward)',
-					'<selection />',
-					'<selection />',
+					'[]',
+					'[]',
 					{ direction: 'backward' }
 					{ direction: 'backward' }
 				);
 				);
 
 
 				test(
 				test(
 					'does nothing on root boundary',
 					'does nothing on root boundary',
-					'<p>foo<selection /></p>',
-					'<p>foo<selection /></p>'
+					'<p>foo[]</p>',
+					'<p>foo[]</p>'
 				);
 				);
 
 
 				test(
 				test(
 					'does nothing on root boundary (backward)',
 					'does nothing on root boundary (backward)',
-					'<p><selection />foo</p>',
-					'<p><selection />foo</p>',
+					'<p>[]foo</p>',
+					'<p>[]foo</p>',
 					{ direction: 'backward' }
 					{ direction: 'backward' }
 				);
 				);
 
 
 				test(
 				test(
 					'extends one character forward',
 					'extends one character forward',
-					'<p>f<selection />oo</p>',
-					'<p>f<selection>o</selection>o</p>'
+					'<p>f[]oo</p>',
+					'<p>f[o]o</p>'
 				);
 				);
 
 
-				test(
-					'extends one character backward',
-					'<p>fo<selection />o</p>',
-					'<p>f<selection backward>o</selection>o</p>',
-					{ direction: 'backward' }
-				);
+				it( 'extends one character backward', () => {
+					setData( document, '<p>fo[]o</p>', { lastRangeBackward: true } );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>f[o]o</p>' );
+					expect( document.selection.isBackward ).to.true;
+				} );
 
 
 				test(
 				test(
 					'extends one character forward (non-collapsed)',
 					'extends one character forward (non-collapsed)',
-					'<p>f<selection>o</selection>obar</p>',
-					'<p>f<selection>oo</selection>bar</p>'
+					'<p>f[o]obar</p>',
+					'<p>f[oo]bar</p>'
 				);
 				);
 
 
-				test(
-					'extends one character backward (non-collapsed)',
-					'<p>foob<selection backward>a</selection>r</p>',
-					'<p>foo<selection backward>ba</selection>r</p>',
-					{ direction: 'backward' }
-				);
+				it( 'extends one character backward (non-collapsed)', () => {
+					setData( document, '<p>foob[a]r</p>', { lastRangeBackward: true } );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>foo[ba]r</p>' );
+					expect( document.selection.isBackward ).to.true;
+				} );
 
 
 				test(
 				test(
 					'extends to element boundary',
 					'extends to element boundary',
-					'<p>fo<selection />o</p>',
-					'<p>fo<selection>o</selection></p>'
+					'<p>fo[]o</p>',
+					'<p>fo[o]</p>'
 				);
 				);
 
 
-				test(
-					'extends to element boundary (backward)',
-					'<p>f<selection />oo</p>',
-					'<p><selection backward>f</selection>oo</p>',
-					{ direction: 'backward' }
-				);
+				it( 'extends to element boundary (backward)', () => {
+					setData( document, '<p>f[]oo</p>' );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[f]oo</p>' );
+					expect( document.selection.isBackward ).to.true;
+				} );
 
 
 				test(
 				test(
 					'shrinks forward selection (to collapsed)',
 					'shrinks forward selection (to collapsed)',
-					'<p>foo<selection>b</selection>ar</p>',
-					'<p>foo<selection />bar</p>',
+					'<p>foo[b]ar</p>',
+					'<p>foo[]bar</p>',
 					{ direction: 'backward' }
 					{ direction: 'backward' }
 				);
 				);
 
 
-				test(
-					'shrinks backward selection (to collapsed)',
-					'<p>foo<selection backward>b</selection>ar</p>',
-					'<p>foob<selection />ar</p>'
-				);
+				it( 'shrinks backward selection (to collapsed)', () => {
+					setData( document, '<p>foo[b]ar</p>', { lastRangeBackward: true } );
+
+					modifySelection( document.selection );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>foob[]ar</p>' );
+					expect( document.selection.isBackward ).to.false;
+				} );
 
 
 				test(
 				test(
 					'extends one element forward',
 					'extends one element forward',
-					'<p>f<selection /><img></img>oo</p>',
-					'<p>f<selection><img></img></selection>oo</p>'
+					'<p>f[]<img></img>oo</p>',
+					'<p>f[<img></img>]oo</p>'
 				);
 				);
 
 
 				test(
 				test(
 					'extends one non-empty element forward',
 					'extends one non-empty element forward',
-					'<p>f<selection /><img>x</img>oo</p>',
-					'<p>f<selection><img>x</img></selection>oo</p>'
+					'<p>f[]<img>x</img>oo</p>',
+					'<p>f[<img>x</img>]oo</p>'
 				);
 				);
 
 
-				test(
-					'extends one element backward',
-					'<p>fo<img></img><selection />o</p>',
-					'<p>fo<selection backward><img></img></selection>o</p>',
-					{ direction: 'backward' }
-				);
+				it( 'extends one element backward', () => {
+					setData( document, '<p>fo<img></img>[]o</p>' );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>fo[<img></img>]o</p>' );
+					expect( document.selection.isBackward ).to.true;
+				} );
 
 
 				test(
 				test(
 					'unicode support - combining mark forward',
 					'unicode support - combining mark forward',
-					'<p>foo<selection />b̂ar</p>',
-					'<p>foo<selection>b̂</selection>ar</p>'
+					'<p>foo[]b̂ar</p>',
+					'<p>foo[b̂]ar</p>'
 				);
 				);
 
 
-				test(
-					'unicode support - combining mark backward',
-					'<p>foob̂<selection />ar</p>',
-					'<p>foo<selection backward>b̂</selection>ar</p>',
-					{ direction: 'backward' }
-				);
+				it( 'unicode support - combining mark backward', () => {
+					setData( document, '<p>foob̂[]ar</p>' );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>foo[b̂]ar</p>' );
+					expect( document.selection.isBackward ).to.true;
+				} );
 
 
 				test(
 				test(
 					'unicode support - combining mark multiple',
 					'unicode support - combining mark multiple',
-					'<p>fo<selection />o̻̐ͩbar</p>',
-					'<p>fo<selection>o̻̐ͩ</selection>bar</p>'
+					'<p>fo[]o̻̐ͩbar</p>',
+					'<p>fo[o̻̐ͩ]bar</p>'
 				);
 				);
 
 
-				test(
-					'unicode support - combining mark multiple backward',
-					'<p>foo̻̐ͩ<selection />bar</p>',
-					'<p>fo<selection backward>o̻̐ͩ</selection>bar</p>',
-					{ direction: 'backward' }
-				);
+				it( 'unicode support - combining mark multiple backward', () => {
+					setData( document, '<p>foo̻̐ͩ[]bar</p>' );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>fo[o̻̐ͩ]bar</p>' );
+					expect( document.selection.isBackward ).to.true;
+				} );
 
 
 				test(
 				test(
 					'unicode support - combining mark to the end',
 					'unicode support - combining mark to the end',
-					'<p>fo<selection />o̻̐ͩ</p>',
-					'<p>fo<selection>o̻̐ͩ</selection></p>'
+					'<p>fo[]o̻̐ͩ</p>',
+					'<p>fo[o̻̐ͩ]</p>'
 				);
 				);
 
 
 				test(
 				test(
 					'unicode support - surrogate pairs forward',
 					'unicode support - surrogate pairs forward',
-					'<p><selection />\uD83D\uDCA9</p>',
-					'<p><selection>\uD83D\uDCA9</selection></p>'
+					'<p>[]\uD83D\uDCA9</p>',
+					'<p>[\uD83D\uDCA9]</p>'
 				);
 				);
 
 
-				test(
-					'unicode support - surrogate pairs backward',
-					'<p>\uD83D\uDCA9<selection /></p>',
-					'<p><selection backward>\uD83D\uDCA9</selection></p>',
-					{ direction: 'backward' }
-				);
+				it( 'unicode support - surrogate pairs backward', () => {
+					setData( document, '<p>\uD83D\uDCA9[]</p>' );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
+					expect( document.selection.isBackward ).to.true;
+				} );
 			} );
 			} );
 
 
 			describe( 'beyond element', () => {
 			describe( 'beyond element', () => {
 				test(
 				test(
 					'extends over boundary of empty elements',
 					'extends over boundary of empty elements',
-					'<p><selection /></p><p></p><p></p>',
-					'<p><selection></p><p></selection></p><p></p>'
+					'<p>[]</p><p></p><p></p>',
+					'<p>[</p><p>]</p><p></p>'
 				);
 				);
 
 
-				test(
-					'extends over boundary of empty elements (backward)',
-					'<p></p><p></p><p><selection /></p>',
-					'<p></p><p><selection backward></p><p></selection></p>',
-					{ direction: 'backward' }
-				);
+				it( 'extends over boundary of empty elements (backward)', () => {
+					setData( document, '<p></p><p></p><p>[]</p>' );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p></p><p>[</p><p>]</p>' );
+					expect( document.selection.isBackward ).to.true;
+				} );
 
 
 				test(
 				test(
 					'extends over boundary of non-empty elements',
 					'extends over boundary of non-empty elements',
-					'<p>a<selection /></p><p>bcd</p>',
-					'<p>a<selection></p><p></selection>bcd</p>'
+					'<p>a[]</p><p>bcd</p>',
+					'<p>a[</p><p>]bcd</p>'
 				);
 				);
 
 
-				test(
-					'extends over boundary of non-empty elements (backward)',
-					'<p>a</p><p><selection />bcd</p>',
-					'<p>a<selection backward></p><p></selection>bcd</p>',
-					{ direction: 'backward' }
-				);
+				it( 'extends over boundary of non-empty elements (backward)', () => {
+					setData( document, '<p>a</p><p>[]bcd</p>' );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>a[</p><p>]bcd</p>' );
+					expect( document.selection.isBackward ).to.true;
+				} );
 
 
 				test(
 				test(
 					'extends over character after boundary',
 					'extends over character after boundary',
-					'<p>a<selection></p><p></selection>bcd</p>',
-					'<p>a<selection></p><p>b</selection>cd</p>'
+					'<p>a[</p><p>]bcd</p>',
+					'<p>a[</p><p>b]cd</p>'
 				);
 				);
 
 
-				test(
-					'extends over character after boundary (backward)',
-					'<p>abc<selection backward></p><p></selection>d</p>',
-					'<p>ab<selection backward>c</p><p></selection>d</p>',
-					{ direction: 'backward' }
-				);
+				it( 'extends over character after boundary (backward)', () => {
+					setData( document, '<p>abc[</p><p>]d</p>', { lastRangeBackward: true } );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>ab[c</p><p>]d</p>' );
+					expect( document.selection.isBackward ).to.true;
+				} );
 
 
 				test(
 				test(
 					'extends over boundary when next element has nested elements',
 					'extends over boundary when next element has nested elements',
-					'<p>a<selection /></p><p><x>bcd</x></p>',
-					'<p>a<selection></p><p></selection><x>bcd</x></p>'
+					'<p>a[]</p><p><x>bcd</x></p>',
+					'<p>a[</p><p>]<x>bcd</x></p>'
 				);
 				);
 
 
 				test(
 				test(
 					'extends over element when next element has nested elements',
 					'extends over element when next element has nested elements',
-					'<p>a<selection></p><p></selection><x>bcd</x>ef</p>',
-					'<p>a<selection></p><p><x>bcd</x></selection>ef</p>'
+					'<p>a[</p><p>]<x>bcd</x>ef</p>',
+					'<p>a[</p><p><x>bcd</x>]ef</p>'
 				);
 				);
 
 
 				test(
 				test(
 					'extends over element when next node is a text',
 					'extends over element when next node is a text',
-					'<p>a<selection /></p>bc',
-					'<p>a<selection></p></selection>bc'
+					'<p>a[]</p>bc',
+					'<p>a[</p>]bc'
 				);
 				);
 
 
-				test(
-					'extends over element when next node is a text (backward)',
-					'ab<p><selection />c</p>',
-					'ab<selection backward><p></selection>c</p>',
-					{ direction: 'backward' }
-				);
+				it( 'extends over element when next node is a text (backward)', () => {
+					setData( document, 'ab<p>[]c</p>' );
 
 
-				test(
-					'shrinks over boundary of empty elements',
-					'<p><selection backward></p><p></selection></p>',
-					'<p></p><p><selection /></p>'
-				);
+					modifySelection( document.selection, { direction: 'backward' } );
 
 
-				test(
-					'shrinks over boundary of empty elements (backward)',
-					'<p><selection></p><p></selection></p>',
-					'<p><selection /></p><p></p>',
-					{ direction: 'backward' }
-				);
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( 'ab[<p>]c</p>' );
+					expect( document.selection.isBackward ).to.true;
+				} );
 
 
-				test(
-					'shrinks over boundary of non-empty elements',
-					'<p>a<selection backward></p><p></selection>b</p>',
-					'<p>a</p><p><selection />b</p>'
-				);
+				it( 'shrinks over boundary of empty elements', () => {
+					setData( document, '<p>[</p><p>]</p>', { lastRangeBackward: true } );
+
+					modifySelection( document.selection );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p></p><p>[]</p>' );
+					expect( document.selection.isBackward ).to.false;
+				} );
+
+				it( 'shrinks over boundary of empty elements (backward)', () => {
+					setData( document, '<p>[</p><p>]</p>' );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[]</p><p></p>' );
+					expect( document.selection.isBackward ).to.false;
+				} );
+
+				it( 'shrinks over boundary of non-empty elements', () => {
+					setData( document, '<p>a[</p><p>]b</p>', { lastRangeBackward: true } );
+
+					modifySelection( document.selection );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>a</p><p>[]b</p>' );
+					expect( document.selection.isBackward ).to.false;
+				} );
 
 
 				test(
 				test(
 					'shrinks over boundary of non-empty elements (backward)',
 					'shrinks over boundary of non-empty elements (backward)',
-					'<p>a<selection></p><p></selection>b</p>',
-					'<p>a<selection /></p><p>b</p>',
+					'<p>a[</p><p>]b</p>',
+					'<p>a[]</p><p>b</p>',
 					{ direction: 'backward' }
 					{ direction: 'backward' }
 				);
 				);
+
+				it( 'updates selection attributes', () => {
+					setData( document, '<p><$text bold="true">foo</$text>[b]</p>' );
+
+					modifySelection( document.selection, { direction: 'backward' } );
+
+					expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p><$text bold="true">foo[]</$text>b</p>' );
+					expect( document.selection.getAttribute( 'bold' ) ).to.equal( 'true' );
+				} );
 			} );
 			} );
 		} );
 		} );
 
 
 		describe( 'unit=codePoint', () => {
 		describe( 'unit=codePoint', () => {
 			test(
 			test(
 				'does nothing on empty content',
 				'does nothing on empty content',
-				'<selection />',
-				'<selection />',
+				'[]',
+				'[]',
 				{ unit: 'codePoint' }
 				{ unit: 'codePoint' }
 			);
 			);
 
 
 			test(
 			test(
 				'does nothing on empty content (with empty element)',
 				'does nothing on empty content (with empty element)',
-				'<p><selection /></p>',
-				'<p><selection /></p>',
+				'<p>[]</p>',
+				'<p>[]</p>',
 				{ unit: 'codePoint' }
 				{ unit: 'codePoint' }
 			);
 			);
 
 
 			test(
 			test(
 				'extends one user-perceived character forward - latin letters',
 				'extends one user-perceived character forward - latin letters',
-				'<p>f<selection />oo</p>',
-				'<p>f<selection>o</selection>o</p>',
+				'<p>f[]oo</p>',
+				'<p>f[o]o</p>',
 				{ unit: 'codePoint' }
 				{ unit: 'codePoint' }
 			);
 			);
 
 
-			test(
-				'extends one user-perceived character backward - latin letters',
-				'<p>fo<selection />o</p>',
-				'<p>f<selection backward>o</selection>o</p>',
-				{ unit: 'codePoint', direction: 'backward' }
-			);
+			it( 'extends one user-perceived character backward - latin letters', () => {
+				setData( document, '<p>fo[]o</p>' );
+
+				modifySelection( document.selection, { unit: 'codePoint', direction: 'backward' } );
+
+				expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>f[o]o</p>' );
+				expect( document.selection.isBackward ).to.true;
+			} );
 
 
 			test(
 			test(
 				'unicode support - combining mark forward',
 				'unicode support - combining mark forward',
-				'<p>foo<selection />b̂ar</p>',
-				'<p>foo<selection>b</selection>̂ar</p>',
+				'<p>foo[]b̂ar</p>',
+				'<p>foo[b]̂ar</p>',
 				{ unit: 'codePoint' }
 				{ unit: 'codePoint' }
 			);
 			);
 
 
-			test(
-				'unicode support - combining mark backward',
-				'<p>foob̂<selection />ar</p>',
-				'<p>foob<selection backward>̂</selection>ar</p>',
-				{ unit: 'codePoint', direction: 'backward' }
-			);
+			it.skip( 'unicode support - combining mark backward', () => {
+				setData( document, '<p>foob[]̂ar</p>' );
+
+				modifySelection( document.selection, { unit: 'codePoint', direction: 'backward' } );
+
+				expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>foob[]̂̂ar</p>' );
+				expect( document.selection.isBackward ).to.true;
+			} );
 
 
 			test(
 			test(
 				'unicode support - combining mark multiple',
 				'unicode support - combining mark multiple',
-				'<p>fo<selection />o̻̐ͩbar</p>',
-				'<p>fo<selection>o</selection>̻̐ͩbar</p>',
+				'<p>fo[]o̻̐ͩbar</p>',
+				'<p>fo[o]̻̐ͩbar</p>',
 				{ unit: 'codePoint' }
 				{ unit: 'codePoint' }
 			);
 			);
 
 
 			test(
 			test(
 				'unicode support - surrogate pairs forward',
 				'unicode support - surrogate pairs forward',
-				'<p><selection />\uD83D\uDCA9</p>',
-				'<p><selection>\uD83D\uDCA9</selection></p>',
+				'<p>[]\uD83D\uDCA9</p>',
+				'<p>[\uD83D\uDCA9]</p>',
 				{ unit: 'codePoint' }
 				{ unit: 'codePoint' }
 			);
 			);
 
 
-			test(
-				'unicode support surrogate pairs backward',
-				'<p>\uD83D\uDCA9<selection /></p>',
-				'<p><selection backward>\uD83D\uDCA9</selection></p>',
-				{ unit: 'codePoint', direction: 'backward' }
-			);
+			it( 'unicode support surrogate pairs backward', () => {
+				setData( document, '<p>\uD83D\uDCA9[]</p>' );
+
+				modifySelection( document.selection, { unit: 'codePoint', direction: 'backward' } );
+
+				expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
+				expect( document.selection.isBackward ).to.true;
+			} );
 		} );
 		} );
 	} );
 	} );
 
 

+ 9 - 9
packages/ckeditor5-engine/tests/model/writer.js

@@ -42,19 +42,19 @@ describe( 'writer', () => {
 		it( 'should insert nodes between nodes', () => {
 		it( 'should insert nodes between nodes', () => {
 			writer.insert( Position.createAt( root, 3 ), [ 'xxx', new Element( 'p' ) ] );
 			writer.insert( Position.createAt( root, 3 ), [ 'xxx', new Element( 'p' ) ] );
 
 
-			expectData( 'fooxxx<p></p><$text bold=true>bar</$text><image src="img.jpg"></image>xyz' );
+			expectData( 'fooxxx<p></p><$text bold="true">bar</$text><image src="img.jpg"></image>xyz' );
 		} );
 		} );
 
 
 		it( 'should split text node if nodes at inserted at offset inside text node', () => {
 		it( 'should split text node if nodes at inserted at offset inside text node', () => {
 			writer.insert( Position.createAt( root, 5 ), new Element( 'p' ) );
 			writer.insert( Position.createAt( root, 5 ), new Element( 'p' ) );
 
 
-			expectData( 'foo<$text bold=true>ba</$text><p></p><$text bold=true>r</$text><image src="img.jpg"></image>xyz' );
+			expectData( 'foo<$text bold="true">ba</$text><p></p><$text bold="true">r</$text><image src="img.jpg"></image>xyz' );
 		} );
 		} );
 
 
 		it( 'should merge text nodes if possible', () => {
 		it( 'should merge text nodes if possible', () => {
 			writer.insert( Position.createAt( root, 3 ), new Text( 'xxx', { bold: true } ) );
 			writer.insert( Position.createAt( root, 3 ), new Text( 'xxx', { bold: true } ) );
 
 
-			expectData( 'foo<$text bold=true>xxxbar</$text><image src="img.jpg"></image>xyz' );
+			expectData( 'foo<$text bold="true">xxxbar</$text><image src="img.jpg"></image>xyz' );
 		} );
 		} );
 	} );
 	} );
 
 
@@ -70,7 +70,7 @@ describe( 'writer', () => {
 			const range = Range.createFromParentsAndOffsets( root, 1, root, 5 );
 			const range = Range.createFromParentsAndOffsets( root, 1, root, 5 );
 			writer.remove( range );
 			writer.remove( range );
 
 
-			expectData( 'f<$text bold=true>r</$text><image src="img.jpg"></image>xyz' );
+			expectData( 'f<$text bold="true">r</$text><image src="img.jpg"></image>xyz' );
 		} );
 		} );
 
 
 		it( 'should merge text nodes if possible', () => {
 		it( 'should merge text nodes if possible', () => {
@@ -93,7 +93,7 @@ describe( 'writer', () => {
 			const range = Range.createFromParentsAndOffsets( root, 3, root, 6 );
 			const range = Range.createFromParentsAndOffsets( root, 3, root, 6 );
 			writer.move( range, Position.createAt( root, 0 ) );
 			writer.move( range, Position.createAt( root, 0 ) );
 
 
-			expectData( '<$text bold=true>bar</$text>foo<image src="img.jpg"></image>xyz' );
+			expectData( '<$text bold="true">bar</$text>foo<image src="img.jpg"></image>xyz' );
 		} );
 		} );
 
 
 		it( 'should use remove and insert methods', () => {
 		it( 'should use remove and insert methods', () => {
@@ -112,7 +112,7 @@ describe( 'writer', () => {
 			const range = Range.createFromParentsAndOffsets( root, 3, root, 6 );
 			const range = Range.createFromParentsAndOffsets( root, 3, root, 6 );
 			writer.move( range, Position.createAt( root, 10 ) );
 			writer.move( range, Position.createAt( root, 10 ) );
 
 
-			expectData( 'foo<image src="img.jpg"></image>xyz<$text bold=true>bar</$text>' );
+			expectData( 'foo<image src="img.jpg"></image>xyz<$text bold="true">bar</$text>' );
 		} );
 		} );
 
 
 		it( 'should throw if given range is not flat', () => {
 		it( 'should throw if given range is not flat', () => {
@@ -127,21 +127,21 @@ describe( 'writer', () => {
 			const range = Range.createFromParentsAndOffsets( root, 6, root, 8 );
 			const range = Range.createFromParentsAndOffsets( root, 6, root, 8 );
 			writer.setAttribute( range, 'newAttr', true );
 			writer.setAttribute( range, 'newAttr', true );
 
 
-			expectData( 'foo<$text bold=true>bar</$text><image newAttr=true src="img.jpg"></image><$text newAttr=true>x</$text>yz' );
+			expectData( 'foo<$text bold="true">bar</$text><image newAttr="true" src="img.jpg"></image><$text newAttr="true">x</$text>yz' );
 		} );
 		} );
 
 
 		it( 'should remove attribute if null was passed as a value', () => {
 		it( 'should remove attribute if null was passed as a value', () => {
 			const range = Range.createFromParentsAndOffsets( root, 6, root, 7 );
 			const range = Range.createFromParentsAndOffsets( root, 6, root, 7 );
 			writer.setAttribute( range, 'src', null );
 			writer.setAttribute( range, 'src', null );
 
 
-			expectData( 'foo<$text bold=true>bar</$text><image></image>xyz' );
+			expectData( 'foo<$text bold="true">bar</$text><image></image>xyz' );
 		} );
 		} );
 
 
 		it( 'should merge nodes if possible', () => {
 		it( 'should merge nodes if possible', () => {
 			const range = Range.createFromParentsAndOffsets( root, 0, root, 3 );
 			const range = Range.createFromParentsAndOffsets( root, 0, root, 3 );
 			writer.setAttribute( range, 'bold', true );
 			writer.setAttribute( range, 'bold', true );
 
 
-			expectData( '<$text bold=true>foobar</$text><image src="img.jpg"></image>xyz' );
+			expectData( '<$text bold="true">foobar</$text><image src="img.jpg"></image>xyz' );
 		} );
 		} );
 	} );
 	} );
 
 

+ 31 - 19
packages/ckeditor5-engine/tests/view/writer/breakAt.js

@@ -47,55 +47,67 @@ describe( 'writer', () => {
 
 
 		it( 'should split attribute element', () => {
 		it( 'should split attribute element', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1>foo{}bar</attribute:b:1></container:p>',
-				'<container:p><attribute:b:1>foo</attribute:b:1>[]<attribute:b:1>bar</attribute:b:1></container:p>'
+				'<container:p><attribute:b view-priority="1">foo{}bar</attribute:b></container:p>',
+				'<container:p>' +
+					'<attribute:b view-priority="1">foo</attribute:b>[]<attribute:b view-priority="1">bar</attribute:b>' +
+				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should move from beginning of the nested text node to the container', () => {
 		it( 'should move from beginning of the nested text node to the container', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1><attribute:u:1>{}foobar</attribute:u:1></attribute:b:1></container:p>',
-				'<container:p>[]<attribute:b:1><attribute:u:1>foobar</attribute:u:1></attribute:b:1></container:p>'
+				'<container:p>' +
+					'<attribute:b view-priority="1"><attribute:u view-priority="1">{}foobar</attribute:u></attribute:b>' +
+				'</container:p>',
+				'<container:p>' +
+					'[]<attribute:b view-priority="1"><attribute:u view-priority="1">foobar</attribute:u></attribute:b>' +
+				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should stick selection in text node if it is in container', () => {
 		it( 'should stick selection in text node if it is in container', () => {
 			test(
 			test(
-				'<container:p>foo{}<attribute:b:1>bar</attribute:b:1></container:p>',
-				'<container:p>foo{}<attribute:b:1>bar</attribute:b:1></container:p>'
+				'<container:p>foo{}<attribute:b view-priority="1">bar</attribute:b></container:p>',
+				'<container:p>foo{}<attribute:b view-priority="1">bar</attribute:b></container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should split nested attributes', () => {
 		it( 'should split nested attributes', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1><attribute:u:1>foo{}bar</attribute:u:1></attribute:b:1></container:p>',
 				'<container:p>' +
 				'<container:p>' +
-					'<attribute:b:1>' +
-						'<attribute:u:1>' +
+					'<attribute:b view-priority="1"><attribute:u view-priority="1">foo{}bar</attribute:u></attribute:b>' +
+				'</container:p>',
+				'<container:p>' +
+					'<attribute:b view-priority="1">' +
+						'<attribute:u view-priority="1">' +
 							'foo' +
 							'foo' +
-						'</attribute:u:1>' +
-					'</attribute:b:1>' +
+						'</attribute:u>' +
+					'</attribute:b>' +
 					'[]' +
 					'[]' +
-					'<attribute:b:1>' +
-						'<attribute:u:1>' +
+					'<attribute:b view-priority="1">' +
+						'<attribute:u view-priority="1">' +
 							'bar' +
 							'bar' +
-						'</attribute:u:1>' +
-					'</attribute:b:1>' +
+						'</attribute:u>' +
+					'</attribute:b>' +
 				'</container:p>'
 				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should move from end of the nested text node to the container', () => {
 		it( 'should move from end of the nested text node to the container', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1><attribute:u:1>foobar{}</attribute:u:1></attribute:b:1></container:p>',
-				'<container:p><attribute:b:1><attribute:u:1>foobar</attribute:u:1></attribute:b:1>[]</container:p>'
+				'<container:p>' +
+					'<attribute:b view-priority="1"><attribute:u view-priority="1">foobar{}</attribute:u></attribute:b>' +
+				'</container:p>',
+				'<container:p>' +
+					'<attribute:b view-priority="1"><attribute:u view-priority="1">foobar</attribute:u></attribute:b>[]' +
+				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should split attribute element directly in document fragment', () => {
 		it( 'should split attribute element directly in document fragment', () => {
 			test(
 			test(
-				'<attribute:b:1>foo{}bar</attribute:b:1>',
-				'<attribute:b:1>foo</attribute:b:1>[]<attribute:b:1>bar</attribute:b:1>'
+				'<attribute:b view-priority="1">foo{}bar</attribute:b>',
+				'<attribute:b view-priority="1">foo</attribute:b>[]<attribute:b view-priority="1">bar</attribute:b>'
 			);
 			);
 		} );
 		} );
 
 

+ 21 - 21
packages/ckeditor5-engine/tests/view/writer/insert.js

@@ -65,8 +65,8 @@ describe( 'writer', () => {
 		it( 'should break attributes when inserting into text node', () => {
 		it( 'should break attributes when inserting into text node', () => {
 			test(
 			test(
 				'<container:p>foo{}bar</container:p>',
 				'<container:p>foo{}bar</container:p>',
-				[ '<attribute:b:1>baz</attribute:b:1>' ],
-				'<container:p>foo[<attribute:b:1>baz</attribute:b:1>]bar</container:p>'
+				[ '<attribute:b view-priority="1">baz</attribute:b>' ],
+				'<container:p>foo[<attribute:b view-priority="1">baz</attribute:b>]bar</container:p>'
 			);
 			);
 		} );
 		} );
 
 
@@ -80,28 +80,28 @@ describe( 'writer', () => {
 
 
 		it( 'should merge same attribute nodes', () => {
 		it( 'should merge same attribute nodes', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1>foo{}bar</attribute:b:1></container:p>',
-				[ '<attribute:b:1>baz</attribute:b:1>' ],
-				'<container:p><attribute:b:1>foo{baz}bar</attribute:b:1></container:p>'
+				'<container:p><attribute:b view-priority="1">foo{}bar</attribute:b></container:p>',
+				[ '<attribute:b view-priority="1">baz</attribute:b>' ],
+				'<container:p><attribute:b view-priority="1">foo{baz}bar</attribute:b></container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should not merge different attributes', () => {
 		it( 'should not merge different attributes', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1>foo{}bar</attribute:b:1></container:p>',
-				[ '<attribute:b:2>baz</attribute:b:2>' ],
+				'<container:p><attribute:b view-priority="1">foo{}bar</attribute:b></container:p>',
+				[ '<attribute:b view-priority="2">baz</attribute:b>' ],
 				'<container:p>' +
 				'<container:p>' +
-					'<attribute:b:1>' +
+					'<attribute:b view-priority="1">' +
 						'foo' +
 						'foo' +
-					'</attribute:b:1>' +
+					'</attribute:b>' +
 					'[' +
 					'[' +
-					'<attribute:b:2>' +
+					'<attribute:b view-priority="2">' +
 						'baz' +
 						'baz' +
-					'</attribute:b:2>' +
+					'</attribute:b>' +
 					']' +
 					']' +
-					'<attribute:b:1>' +
+					'<attribute:b view-priority="1">' +
 						'bar' +
 						'bar' +
-					'</attribute:b:1>' +
+					'</attribute:b>' +
 				'</container:p>'
 				'</container:p>'
 			);
 			);
 		} );
 		} );
@@ -109,16 +109,16 @@ describe( 'writer', () => {
 		it( 'should allow to insert multiple nodes', () => {
 		it( 'should allow to insert multiple nodes', () => {
 			test(
 			test(
 				'<container:p>[]</container:p>',
 				'<container:p>[]</container:p>',
-				[ '<attribute:b:1>foo</attribute:b:1>', 'bar' ],
-				'<container:p>[<attribute:b:1>foo</attribute:b:1>bar]</container:p>'
+				[ '<attribute:b view-priority="1">foo</attribute:b>', 'bar' ],
+				'<container:p>[<attribute:b view-priority="1">foo</attribute:b>bar]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should merge after inserting multiple nodes', () => {
 		it( 'should merge after inserting multiple nodes', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1>qux</attribute:b:1>[]baz</container:p>',
-				[ '<attribute:b:1>foo</attribute:b:1>', 'bar' ],
-				'<container:p><attribute:b:1>qux{foo</attribute:b:1>bar}baz</container:p>'
+				'<container:p><attribute:b view-priority="1">qux</attribute:b>[]baz</container:p>',
+				[ '<attribute:b view-priority="1">foo</attribute:b>', 'bar' ],
+				'<container:p><attribute:b view-priority="1">qux{foo</attribute:b>bar}baz</container:p>'
 			);
 			);
 		} );
 		} );
 
 
@@ -132,9 +132,9 @@ describe( 'writer', () => {
 
 
 		it( 'should merge same attribute nodes in document fragment', () => {
 		it( 'should merge same attribute nodes in document fragment', () => {
 			test(
 			test(
-				'<attribute:b:2>foo</attribute:b:2>[]',
-				[ '<attribute:b:1>bar</attribute:b:1>' ],
-				'<attribute:b:2>foo</attribute:b:2>[<attribute:b:1>bar</attribute:b:1>]'
+				'<attribute:b view-priority="2">foo</attribute:b>[]',
+				[ '<attribute:b view-priority="1">bar</attribute:b>' ],
+				'<attribute:b view-priority="2">foo</attribute:b>[<attribute:b view-priority="1">bar</attribute:b>]'
 			);
 			);
 		} );
 		} );
 
 

+ 25 - 13
packages/ckeditor5-engine/tests/view/writer/mergeAt.js

@@ -46,15 +46,15 @@ describe( 'writer', () => {
 
 
 		it( 'should not merge when position is placed at the beginning of the container', () => {
 		it( 'should not merge when position is placed at the beginning of the container', () => {
 			test(
 			test(
-				'<container:p>[]<attribute:b:1></attribute:b:1></container:p>',
-				'<container:p>[]<attribute:b:1></attribute:b:1></container:p>'
+				'<container:p>[]<attribute:b view-priority="1"></attribute:b></container:p>',
+				'<container:p>[]<attribute:b view-priority="1"></attribute:b></container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should not merge when position is placed at the end of the container', () => {
 		it( 'should not merge when position is placed at the end of the container', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1></attribute:b:1>[]</container:p>',
-				'<container:p><attribute:b:1></attribute:b:1>[]</container:p>'
+				'<container:p><attribute:b view-priority="1"></attribute:b>[]</container:p>',
+				'<container:p><attribute:b view-priority="1"></attribute:b>[]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
@@ -71,29 +71,41 @@ describe( 'writer', () => {
 
 
 		it( 'should merge when placed between similar attribute nodes', () => {
 		it( 'should merge when placed between similar attribute nodes', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1 foo="bar">baz</attribute:b:1>[]<attribute:b:1 foo="bar">qux</attribute:b:1></container:p>',
-				'<container:p><attribute:b:1 foo="bar">baz{}qux</attribute:b:1></container:p>'
+				'<container:p>' +
+					'<attribute:b view-priority="1" foo="bar">baz</attribute:b>[]<attribute:b view-priority="1" foo="bar">qux</attribute:b>' +
+				'</container:p>',
+				'<container:p><attribute:b view-priority="1" foo="bar">baz{}qux</attribute:b></container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should not merge when placed between non-similar attribute nodes', () => {
 		it( 'should not merge when placed between non-similar attribute nodes', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1 foo="bar"></attribute:b:1>[]<attribute:b:1 foo="baz"></attribute:b:1></container:p>',
-				'<container:p><attribute:b:1 foo="bar"></attribute:b:1>[]<attribute:b:1 foo="baz"></attribute:b:1></container:p>'
+				'<container:p>' +
+					'<attribute:b view-priority="1" foo="bar"></attribute:b>[]<attribute:b view-priority="1" foo="baz"></attribute:b>' +
+				'</container:p>',
+				'<container:p>' +
+					'<attribute:b view-priority="1" foo="bar"></attribute:b>[]<attribute:b view-priority="1" foo="baz"></attribute:b>' +
+				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should not merge when placed between similar attribute nodes with different priority', () => {
 		it( 'should not merge when placed between similar attribute nodes with different priority', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1 foo="bar"></attribute:b:1>[]<attribute:b:2 foo="bar"></attribute:b:2></container:p>',
-				'<container:p><attribute:b:1 foo="bar"></attribute:b:1>[]<attribute:b:2 foo="bar"></attribute:b:2></container:p>'
+				'<container:p>' +
+					'<attribute:b view-priority="1" foo="bar"></attribute:b>[]<attribute:b view-priority="2" foo="bar"></attribute:b>' +
+				'</container:p>',
+				'<container:p>' +
+					'<attribute:b view-priority="1" foo="bar"></attribute:b>[]<attribute:b view-priority="2" foo="bar"></attribute:b>' +
+				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should merge attribute nodes and their contents if possible', () => {
 		it( 'should merge attribute nodes and their contents if possible', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1 foo="bar">foo</attribute:b:1>[]<attribute:b:1 foo="bar">bar</attribute:b:1></container:p>',
-				'<container:p><attribute:b:1 foo="bar">foo{}bar</attribute:b:1></container:p>'
+				'<container:p>' +
+					'<attribute:b view-priority="1" foo="bar">foo</attribute:b>[]<attribute:b view-priority="1" foo="bar">bar</attribute:b>' +
+				'</container:p>',
+				'<container:p><attribute:b view-priority="1" foo="bar">foo{}bar</attribute:b></container:p>'
 			);
 			);
 		} );
 		} );
 
 
@@ -107,7 +119,7 @@ describe( 'writer', () => {
 		it( 'should remove empty attributes after merge #2', () => {
 		it( 'should remove empty attributes after merge #2', () => {
 			test(
 			test(
 				'<container:p><attribute:b>foo</attribute:b><attribute:i>[]</attribute:i><attribute:b>bar</attribute:b></container:p>',
 				'<container:p><attribute:b>foo</attribute:b><attribute:i>[]</attribute:i><attribute:b>bar</attribute:b></container:p>',
-				'<container:p><attribute:b:10>foo{}bar</attribute:b:10></container:p>'
+				'<container:p><attribute:b view-priority="10">foo{}bar</attribute:b></container:p>'
 			);
 			);
 		} );
 		} );
 
 

+ 20 - 12
packages/ckeditor5-engine/tests/view/writer/move.js

@@ -66,28 +66,36 @@ describe( 'writer', () => {
 
 
 		it( 'should move parts of nodes', () => {
 		it( 'should move parts of nodes', () => {
 			test(
 			test(
-				'<container:p>f{oo<attribute:b:10>ba}r</attribute:b:10></container:p>',
-				'<container:p>[]<attribute:b:10>qux</attribute:b:10></container:p>',
-				'<container:p>f<attribute:b:10>r</attribute:b:10></container:p>',
-				'<container:p>[oo<attribute:b:10>ba}qux</attribute:b:10></container:p>'
+				'<container:p>f{oo<attribute:b view-priority="10">ba}r</attribute:b></container:p>',
+				'<container:p>[]<attribute:b view-priority="10">qux</attribute:b></container:p>',
+				'<container:p>f<attribute:b view-priority="10">r</attribute:b></container:p>',
+				'<container:p>[oo<attribute:b view-priority="10">ba}qux</attribute:b></container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should merge after moving #1', () => {
 		it( 'should merge after moving #1', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1>foo</attribute:b:1>[bar]<attribute:b:1>bazqux</attribute:b:1></container:p>',
-				'<container:p><attribute:b:1>foo{}bazqux</attribute:b:1></container:p>',
-				'<container:p><attribute:b:1>foobazqux</attribute:b:1></container:p>',
-				'<container:p><attribute:b:1>foo</attribute:b:1>[bar]<attribute:b:1>bazqux</attribute:b:1></container:p>'
+				'<container:p>' +
+					'<attribute:b view-priority="1">foo</attribute:b>[bar]<attribute:b view-priority="1">bazqux</attribute:b>' +
+				'</container:p>',
+				'<container:p><attribute:b view-priority="1">foo{}bazqux</attribute:b></container:p>',
+				'<container:p><attribute:b view-priority="1">foobazqux</attribute:b></container:p>',
+				'<container:p>' +
+					'<attribute:b view-priority="1">foo</attribute:b>[bar]<attribute:b view-priority="1">bazqux</attribute:b>' +
+				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should merge after moving #2', () => {
 		it( 'should merge after moving #2', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1>fo{o</attribute:b:1>bar<attribute:b:1>ba}zqux</attribute:b:1></container:p>',
-				'<container:p><attribute:b:1>fo{}zqux</attribute:b:1></container:p>',
-				'<container:p><attribute:b:1>fozqux</attribute:b:1></container:p>',
-				'<container:p><attribute:b:1>fo{o</attribute:b:1>bar<attribute:b:1>ba}zqux</attribute:b:1></container:p>'
+				'<container:p>' +
+					'<attribute:b view-priority="1">fo{o</attribute:b>bar<attribute:b view-priority="1">ba}zqux</attribute:b>' +
+				'</container:p>',
+				'<container:p><attribute:b view-priority="1">fo{}zqux</attribute:b></container:p>',
+				'<container:p><attribute:b view-priority="1">fozqux</attribute:b></container:p>',
+				'<container:p>' +
+					'<attribute:b view-priority="1">fo{o</attribute:b>bar<attribute:b view-priority="1">ba}zqux</attribute:b>' +
+				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 

+ 15 - 11
packages/ckeditor5-engine/tests/view/writer/remove.js

@@ -73,33 +73,37 @@ describe( 'writer', () => {
 
 
 		it( 'should remove parts of nodes', () => {
 		it( 'should remove parts of nodes', () => {
 			test(
 			test(
-				'<container:p>f{oo<attribute:b:10>ba}r</attribute:b:10></container:p>',
-				'<container:p>f[]<attribute:b:10>r</attribute:b:10></container:p>',
-				'oo<attribute:b:10>ba</attribute:b:10>'
+				'<container:p>f{oo<attribute:b view-priority="10">ba}r</attribute:b></container:p>',
+				'<container:p>f[]<attribute:b view-priority="10">r</attribute:b></container:p>',
+				'oo<attribute:b view-priority="10">ba</attribute:b>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should support unicode', () => {
 		it( 'should support unicode', () => {
 			test(
 			test(
-				'<container:p>நி{லை<attribute:b:10>க்}கு</attribute:b:10></container:p>',
-				'<container:p>நி[]<attribute:b:10>கு</attribute:b:10></container:p>',
-				'லை<attribute:b:10>க்</attribute:b:10>'
+				'<container:p>நி{லை<attribute:b view-priority="10">க்}கு</attribute:b></container:p>',
+				'<container:p>நி[]<attribute:b view-priority="10">கு</attribute:b></container:p>',
+				'லை<attribute:b view-priority="10">க்</attribute:b>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should merge after removing #1', () => {
 		it( 'should merge after removing #1', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1>foo</attribute:b:1>[bar]<attribute:b:1>bazqux</attribute:b:1></container:p>',
-				'<container:p><attribute:b:1>foo{}bazqux</attribute:b:1></container:p>',
+				'<container:p>' +
+					'<attribute:b view-priority="1">foo</attribute:b>[bar]<attribute:b view-priority="1">bazqux</attribute:b>' +
+				'</container:p>',
+				'<container:p><attribute:b view-priority="1">foo{}bazqux</attribute:b></container:p>',
 				'bar'
 				'bar'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should merge after removing #2', () => {
 		it( 'should merge after removing #2', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1>fo{o</attribute:b:1>bar<attribute:b:1>ba}zqux</attribute:b:1></container:p>',
-				'<container:p><attribute:b:1>fo{}zqux</attribute:b:1></container:p>',
-				'<attribute:b:1>o</attribute:b:1>bar<attribute:b:1>ba</attribute:b:1>'
+				'<container:p>' +
+					'<attribute:b view-priority="1">fo{o</attribute:b>bar<attribute:b view-priority="1">ba}zqux</attribute:b>' +
+				'</container:p>',
+				'<container:p><attribute:b view-priority="1">fo{}zqux</attribute:b></container:p>',
+				'<attribute:b view-priority="1">o</attribute:b>bar<attribute:b view-priority="1">ba</attribute:b>'
 			);
 			);
 		} );
 		} );
 
 

+ 84 - 78
packages/ckeditor5-engine/tests/view/writer/unwrap.js

@@ -34,7 +34,7 @@ describe( 'writer', () => {
 		it( 'should do nothing on collapsed ranges', () => {
 		it( 'should do nothing on collapsed ranges', () => {
 			test(
 			test(
 				'<container:p>f{}oo</container:p>',
 				'<container:p>f{}oo</container:p>',
-				'<attribute:b:10></attribute:b:10>',
+				'<attribute:b view-priority="10"></attribute:b>',
 				'<container:p>f{}oo</container:p>'
 				'<container:p>f{}oo</container:p>'
 			);
 			);
 		} );
 		} );
@@ -42,7 +42,7 @@ describe( 'writer', () => {
 		it( 'should do nothing on single text node', () => {
 		it( 'should do nothing on single text node', () => {
 			test(
 			test(
 				'<container:p>[foobar]</container:p>',
 				'<container:p>[foobar]</container:p>',
-				'<attribute:b:1></attribute:b:1>',
+				'<attribute:b view-priority="1"></attribute:b>',
 				'<container:p>[foobar]</container:p>'
 				'<container:p>[foobar]</container:p>'
 			);
 			);
 		} );
 		} );
@@ -85,17 +85,17 @@ describe( 'writer', () => {
 
 
 		it( 'should unwrap single node', () => {
 		it( 'should unwrap single node', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1>foobar</attribute:b:1>]</container:p>',
-				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[<attribute:b view-priority="1">foobar</attribute:b>]</container:p>',
+				'<attribute:b view-priority="1"></attribute:b>',
 				'<container:p>[foobar]</container:p>'
 				'<container:p>[foobar]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should not unwrap attributes with different priorities #1', () => {
 		it( 'should not unwrap attributes with different priorities #1', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1>foobar</attribute:b:1>]</container:p>',
-				'<attribute:b:2></attribute:b:2>',
-				'<container:p>[<attribute:b:1>foobar</attribute:b:1>]</container:p>'
+				'<container:p>[<attribute:b view-priority="1">foobar</attribute:b>]</container:p>',
+				'<attribute:b view-priority="2"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1">foobar</attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
@@ -103,44 +103,44 @@ describe( 'writer', () => {
 			test(
 			test(
 				'<container:p>' +
 				'<container:p>' +
 				'[' +
 				'[' +
-					'<attribute:b:2>foo</attribute:b:2>' +
-					'<attribute:b:1>bar</attribute:b:1>' +
-					'<attribute:b:2>baz</attribute:b:2>' +
+					'<attribute:b view-priority="2">foo</attribute:b>' +
+					'<attribute:b view-priority="1">bar</attribute:b>' +
+					'<attribute:b view-priority="2">baz</attribute:b>' +
 				']' +
 				']' +
 				'</container:p>',
 				'</container:p>',
-				'<attribute:b:2></attribute:b:2>',
-				'<container:p>[foo<attribute:b:1>bar</attribute:b:1>baz]</container:p>'
+				'<attribute:b view-priority="2"></attribute:b>',
+				'<container:p>[foo<attribute:b view-priority="1">bar</attribute:b>baz]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should unwrap part of the node', () => {
 		it( 'should unwrap part of the node', () => {
 			test(
 			test(
-				'<container:p>[baz<attribute:b:1>foo}bar</attribute:b:1>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p>[bazfoo]<attribute:b:1>bar</attribute:b:1></container:p>'
+				'<container:p>[baz<attribute:b view-priority="1">foo}bar</attribute:b></container:p>',
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p>[bazfoo]<attribute:b view-priority="1">bar</attribute:b></container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should support unicode', () => {
 		it( 'should support unicode', () => {
 			test(
 			test(
-				'<container:p>[நிலை<attribute:b:1>க்}கு</attribute:b:1>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p>[நிலைக்]<attribute:b:1>கு</attribute:b:1></container:p>'
+				'<container:p>[நிலை<attribute:b view-priority="1">க்}கு</attribute:b></container:p>',
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p>[நிலைக்]<attribute:b view-priority="1">கு</attribute:b></container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should unwrap nested attributes', () => {
 		it( 'should unwrap nested attributes', () => {
 			test(
 			test(
-				'<container:p>[<attribute:u:1><attribute:b:1>foobar</attribute:b:1></attribute:u:1>]</container:p>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p>[<attribute:u:1>foobar</attribute:u:1>]</container:p>'
+				'<container:p>[<attribute:u view-priority="1"><attribute:b view-priority="1">foobar</attribute:b></attribute:u>]</container:p>',
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p>[<attribute:u view-priority="1">foobar</attribute:u>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should merge unwrapped nodes #1', () => {
 		it( 'should merge unwrapped nodes #1', () => {
 			test(
 			test(
-				'<container:p>foo[<attribute:b:1>bar</attribute:b:1>]baz</container:p>',
-				'<attribute:b:1></attribute:b:1>',
+				'<container:p>foo[<attribute:b view-priority="1">bar</attribute:b>]baz</container:p>',
+				'<attribute:b view-priority="1"></attribute:b>',
 				'<container:p>foo{bar}baz</container:p>'
 				'<container:p>foo{bar}baz</container:p>'
 			);
 			);
 		} );
 		} );
@@ -148,15 +148,15 @@ describe( 'writer', () => {
 		it( 'should merge unwrapped nodes #2', () => {
 		it( 'should merge unwrapped nodes #2', () => {
 			const input = '<container:p>' +
 			const input = '<container:p>' +
 			'foo' +
 			'foo' +
-				'<attribute:u:1>bar</attribute:u:1>' +
+				'<attribute:u view-priority="1">bar</attribute:u>' +
 				'[' +
 				'[' +
-				'<attribute:b:1>' +
-					'<attribute:u:1>bazqux</attribute:u:1>' +
-				'</attribute:b:1>' +
+				'<attribute:b view-priority="1">' +
+					'<attribute:u view-priority="1">bazqux</attribute:u>' +
+				'</attribute:b>' +
 				']' +
 				']' +
 			'</container:p>';
 			'</container:p>';
-			const attribute = '<attribute:b:1></attribute:b:1>';
-			const result = '<container:p>foo<attribute:u:1>bar{bazqux</attribute:u:1>]</container:p>';
+			const attribute = '<attribute:b view-priority="1"></attribute:b>';
+			const result = '<container:p>foo<attribute:u view-priority="1">bar{bazqux</attribute:u>]</container:p>';
 
 
 			test( input, attribute, result );
 			test( input, attribute, result );
 		} );
 		} );
@@ -164,19 +164,19 @@ describe( 'writer', () => {
 		it( 'should merge unwrapped nodes #3', () => {
 		it( 'should merge unwrapped nodes #3', () => {
 			const input = '<container:p>' +
 			const input = '<container:p>' +
 				'foo' +
 				'foo' +
-				'<attribute:u:1>bar</attribute:u:1>' +
+				'<attribute:u view-priority="1">bar</attribute:u>' +
 				'[' +
 				'[' +
-				'<attribute:b:1>' +
-					'<attribute:u:1>baz}qux</attribute:u:1>' +
-				'</attribute:b:1>' +
+				'<attribute:b view-priority="1">' +
+					'<attribute:u view-priority="1">baz}qux</attribute:u>' +
+				'</attribute:b>' +
 			'</container:p>';
 			'</container:p>';
-			const attribute = '<attribute:b:1></attribute:b:1>';
+			const attribute = '<attribute:b view-priority="1"></attribute:b>';
 			const result = '<container:p>' +
 			const result = '<container:p>' +
 				'foo' +
 				'foo' +
-				'<attribute:u:1>bar{baz</attribute:u:1>]' +
-				'<attribute:b:1>' +
-					'<attribute:u:1>qux</attribute:u:1>' +
-				'</attribute:b:1>' +
+				'<attribute:u view-priority="1">bar{baz</attribute:u>]' +
+				'<attribute:b view-priority="1">' +
+					'<attribute:u view-priority="1">qux</attribute:u>' +
+				'</attribute:b>' +
 			'</container:p>';
 			'</container:p>';
 
 
 			test( input, attribute, result );
 			test( input, attribute, result );
@@ -185,18 +185,18 @@ describe( 'writer', () => {
 		it( 'should merge unwrapped nodes #4', () => {
 		it( 'should merge unwrapped nodes #4', () => {
 			const input = '<container:p>' +
 			const input = '<container:p>' +
 				'foo' +
 				'foo' +
-				'<attribute:u:1>bar</attribute:u:1>' +
+				'<attribute:u view-priority="1">bar</attribute:u>' +
 				'[' +
 				'[' +
-				'<attribute:b:1>' +
-					'<attribute:u:1>baz</attribute:u:1>' +
-				'</attribute:b:1>' +
+				'<attribute:b view-priority="1">' +
+					'<attribute:u view-priority="1">baz</attribute:u>' +
+				'</attribute:b>' +
 				']' +
 				']' +
-				'<attribute:u:1>qux</attribute:u:1>' +
+				'<attribute:u view-priority="1">qux</attribute:u>' +
 			'</container:p>';
 			'</container:p>';
-			const attribute = '<attribute:b:1></attribute:b:1>';
+			const attribute = '<attribute:b view-priority="1"></attribute:b>';
 			const result = '<container:p>' +
 			const result = '<container:p>' +
 				'foo' +
 				'foo' +
-				'<attribute:u:1>bar{baz}qux</attribute:u:1>' +
+				'<attribute:u view-priority="1">bar{baz}qux</attribute:u>' +
 			'</container:p>';
 			'</container:p>';
 
 
 			test( input, attribute, result );
 			test( input, attribute, result );
@@ -205,13 +205,13 @@ describe( 'writer', () => {
 		it( 'should merge unwrapped nodes #5', () => {
 		it( 'should merge unwrapped nodes #5', () => {
 			const input = '<container:p>' +
 			const input = '<container:p>' +
 				'[' +
 				'[' +
-				'<attribute:b:1><attribute:u:1>foo</attribute:u:1></attribute:b:1>' +
-				'<attribute:b:1><attribute:u:1>bar</attribute:u:1></attribute:b:1>' +
-				'<attribute:b:1><attribute:u:1>baz</attribute:u:1></attribute:b:1>' +
+				'<attribute:b view-priority="1"><attribute:u view-priority="1">foo</attribute:u></attribute:b>' +
+				'<attribute:b view-priority="1"><attribute:u view-priority="1">bar</attribute:u></attribute:b>' +
+				'<attribute:b view-priority="1"><attribute:u view-priority="1">baz</attribute:u></attribute:b>' +
 				']' +
 				']' +
 			'</container:p>';
 			'</container:p>';
-			const attribute = '<attribute:b:1></attribute:b:1>';
-			const result = '<container:p>[<attribute:u:1>foobarbaz</attribute:u:1>]</container:p>';
+			const attribute = '<attribute:b view-priority="1"></attribute:b>';
+			const result = '<container:p>[<attribute:u view-priority="1">foobarbaz</attribute:u>]</container:p>';
 
 
 			test( input, attribute, result );
 			test( input, attribute, result );
 		} );
 		} );
@@ -219,76 +219,82 @@ describe( 'writer', () => {
 		it( 'should unwrap mixed ranges #1', () => {
 		it( 'should unwrap mixed ranges #1', () => {
 			const input = '<container:p>' +
 			const input = '<container:p>' +
 				'[' +
 				'[' +
-				'<attribute:u:1>' +
-					'<attribute:b:1>foo]</attribute:b:1>' +
-				'</attribute:u:1>' +
+				'<attribute:u view-priority="1">' +
+					'<attribute:b view-priority="1">foo]</attribute:b>' +
+				'</attribute:u>' +
 			'</container:p>';
 			'</container:p>';
-			const attribute = '<attribute:b:1></attribute:b:1>';
-			const result = '<container:p>[<attribute:u:1>foo</attribute:u:1>]</container:p>';
+			const attribute = '<attribute:b view-priority="1"></attribute:b>';
+			const result = '<container:p>[<attribute:u view-priority="1">foo</attribute:u>]</container:p>';
 
 
 			test( input, attribute, result );
 			test( input, attribute, result );
 		} );
 		} );
 
 
 		it( 'should unwrap mixed ranges #2', () => {
 		it( 'should unwrap mixed ranges #2', () => {
 			test(
 			test(
-				'<container:p>[<attribute:u:1><attribute:b:1>foo}</attribute:b:1></attribute:u></container:p>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p>[<attribute:u:1>foo</attribute:u:1>]</container:p>'
+				'<container:p>[<attribute:u view-priority="1"><attribute:b view-priority="1">foo}</attribute:b></attribute:u></container:p>',
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p>[<attribute:u view-priority="1">foo</attribute:u>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should unwrap single element by removing matching attributes', () => {
 		it( 'should unwrap single element by removing matching attributes', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1 foo="bar" baz="qux">test</attribute:b:1>]</container:p>',
-				'<attribute:b:1 baz="qux"></attribute:b:1>',
-				'<container:p>[<attribute:b:1 foo="bar">test</attribute:b:1>]</container:p>'
+				'<container:p>[<attribute:b view-priority="1" foo="bar" baz="qux">test</attribute:b>]</container:p>',
+				'<attribute:b view-priority="1" baz="qux"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1" foo="bar">test</attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should not unwrap single element when attributes are different', () => {
 		it( 'should not unwrap single element when attributes are different', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1 baz="qux" foo="bar">test</attribute:b:1>]</container:p>',
-				'<attribute:b:1 baz="qux" test="true"></attribute:b:1>',
-				'<container:p>[<attribute:b:1 baz="qux" foo="bar">test</attribute:b:1>]</container:p>'
+				'<container:p>[<attribute:b view-priority="1" baz="qux" foo="bar">test</attribute:b>]</container:p>',
+				'<attribute:b view-priority="1" baz="qux" test="true"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1" baz="qux" foo="bar">test</attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should unwrap single element by removing matching classes', () => {
 		it( 'should unwrap single element by removing matching classes', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1 class="foo bar baz">test</attribute:b:1>]</container:p>',
-				'<attribute:b:1 class="baz foo"></attribute:b:1>',
-				'<container:p>[<attribute:b:1 class="bar">test</attribute:b:1>]</container:p>'
+				'<container:p>[<attribute:b view-priority="1" class="foo bar baz">test</attribute:b>]</container:p>',
+				'<attribute:b view-priority="1" class="baz foo"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1" class="bar">test</attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should not unwrap single element when classes are different', () => {
 		it( 'should not unwrap single element when classes are different', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1 class="foo bar baz">test</attribute:b:1>]</container:p>',
-				'<attribute:b:1 class="baz foo qux"></attribute:b:1>',
-				'<container:p>[<attribute:b:1 class="foo bar baz">test</attribute:b:1>]</container:p>'
+				'<container:p>[<attribute:b view-priority="1" class="foo bar baz">test</attribute:b>]</container:p>',
+				'<attribute:b view-priority="1" class="baz foo qux"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1" class="foo bar baz">test</attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should unwrap single element by removing matching styles', () => {
 		it( 'should unwrap single element by removing matching styles', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1 style="color:red;position:absolute;top:10px;">test</attribute:b:1>]</container:p>',
-				'<attribute:b:1 style="position: absolute;"></attribute:b:1>',
-				'<container:p>[<attribute:b:1 style="color:red;top:10px;">test</attribute:b:1>]</container:p>'
+				'<container:p>' +
+					'[<attribute:b view-priority="1" style="color:red;position:absolute;top:10px;">test</attribute:b>]' +
+				'</container:p>',
+				'<attribute:b view-priority="1" style="position: absolute;"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1" style="color:red;top:10px;">test</attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should not unwrap single element when styles are different', () => {
 		it( 'should not unwrap single element when styles are different', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1 style="color:red;position:absolute;top:10px;">test</attribute:b:1>]</container:p>',
-				'<attribute:b:1 style="position: relative;"></attribute:b:1>',
-				'<container:p>[<attribute:b:1 style="color:red;position:absolute;top:10px;">test</attribute:b:1>]</container:p>'
+				'<container:p>' +
+					'[<attribute:b view-priority="1" style="color:red;position:absolute;top:10px;">test</attribute:b>]' +
+				'</container:p>',
+				'<attribute:b view-priority="1" style="position: relative;"></attribute:b>',
+				'<container:p>' +
+					'[<attribute:b view-priority="1" style="color:red;position:absolute;top:10px;">test</attribute:b>]' +
+				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should unwrap single node in document fragment', () => {
 		it( 'should unwrap single node in document fragment', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1>foobar</attribute:b:1>]</container:p>',
-				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[<attribute:b view-priority="1">foobar</attribute:b>]</container:p>',
+				'<attribute:b view-priority="1"></attribute:b>',
 				'<container:p>[foobar]</container:p>'
 				'<container:p>[foobar]</container:p>'
 			);
 			);
 		} );
 		} );

+ 73 - 63
packages/ckeditor5-engine/tests/view/writer/wrap.js

@@ -42,16 +42,16 @@ describe( 'writer', () => {
 		it( 'wraps single text node', () => {
 		it( 'wraps single text node', () => {
 			test(
 			test(
 				'<container:p>[foobar]</container:p>',
 				'<container:p>[foobar]</container:p>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p>[<attribute:b:1>foobar</attribute:b:1>]</container:p>'
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1">foobar</attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'wraps single text node in document fragment', () => {
 		it( 'wraps single text node in document fragment', () => {
 			test(
 			test(
 				'{foobar}',
 				'{foobar}',
-				'<attribute:b:1></attribute:b:1>',
-				'[<attribute:b:1>foobar</attribute:b:1>]'
+				'<attribute:b view-priority="1"></attribute:b>',
+				'[<attribute:b view-priority="1">foobar</attribute:b>]'
 			);
 			);
 		} );
 		} );
 
 
@@ -94,94 +94,100 @@ describe( 'writer', () => {
 		it( 'wraps part of a single text node #1', () => {
 		it( 'wraps part of a single text node #1', () => {
 			test(
 			test(
 				'<container:p>[foo}bar</container:p>',
 				'<container:p>[foo}bar</container:p>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p>[<attribute:b:1>foo</attribute:b:1>]bar</container:p>'
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1">foo</attribute:b>]bar</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'wraps part of a single text node #2', () => {
 		it( 'wraps part of a single text node #2', () => {
 			test(
 			test(
 				'<container:p>{foo}bar</container:p>',
 				'<container:p>{foo}bar</container:p>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p>[<attribute:b:1>foo</attribute:b:1>]bar</container:p>'
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1">foo</attribute:b>]bar</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should support unicode', () => {
 		it( 'should support unicode', () => {
 			test(
 			test(
 				'<container:p>நி{லை}க்கு</container:p>',
 				'<container:p>நி{லை}க்கு</container:p>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p>நி[<attribute:b:1>லை</attribute:b:1>]க்கு</container:p>'
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p>நி[<attribute:b view-priority="1">லை</attribute:b>]க்கு</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'wraps part of a single text node #3', () => {
 		it( 'wraps part of a single text node #3', () => {
 			test(
 			test(
 				'<container:p>foo{bar}</container:p>',
 				'<container:p>foo{bar}</container:p>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p>foo[<attribute:b:1>bar</attribute:b:1>]</container:p>'
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p>foo[<attribute:b view-priority="1">bar</attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should not wrap inside nested containers', () => {
 		it( 'should not wrap inside nested containers', () => {
 			test(
 			test(
 				'<container:div>[foobar<container:p>baz</container:p>]</container:div>',
 				'<container:div>[foobar<container:p>baz</container:p>]</container:div>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:div>[<attribute:b:1>foobar</attribute:b:1><container:p>baz</container:p>]</container:div>'
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:div>[<attribute:b view-priority="1">foobar</attribute:b><container:p>baz</container:p>]</container:div>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'wraps according to priorities', () => {
 		it( 'wraps according to priorities', () => {
 			test(
 			test(
-				'<container:p>[<attribute:u:1>foobar</attribute:u:1>]</container:p>',
-				'<attribute:b:2></attribute:b:2>',
-				'<container:p>[<attribute:u:1><attribute:b:2>foobar</attribute:b:2></attribute:u:1>]</container:p>'
+				'<container:p>[<attribute:u view-priority="1">foobar</attribute:u>]</container:p>',
+				'<attribute:b view-priority="2"></attribute:b>',
+				'<container:p>' +
+					'[<attribute:u view-priority="1"><attribute:b view-priority="2">foobar</attribute:b></attribute:u>]' +
+				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'merges wrapped nodes #1', () => {
 		it( 'merges wrapped nodes #1', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1>foo</attribute:b:1>bar<attribute:b:1>baz</attribute:b:1>]</container:p>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p>[<attribute:b:1>foobarbaz</attribute:b:1>]</container:p>'
+				'<container:p>' +
+					'[<attribute:b view-priority="1">foo</attribute:b>bar<attribute:b view-priority="1">baz</attribute:b>]' +
+				'</container:p>',
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1">foobarbaz</attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'merges wrapped nodes #2', () => {
 		it( 'merges wrapped nodes #2', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1>foo</attribute:b:1>[bar}baz</container:p>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p><attribute:b:1>foo{bar</attribute:b:1>]baz</container:p>'
+				'<container:p><attribute:b view-priority="1">foo</attribute:b>[bar}baz</container:p>',
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p><attribute:b view-priority="1">foo{bar</attribute:b>]baz</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'merges wrapped nodes #3', () => {
 		it( 'merges wrapped nodes #3', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1>foobar</attribute:b:1>[baz]</container:p>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p><attribute:b:1>foobar{baz</attribute:b:1>]</container:p>'
+				'<container:p><attribute:b view-priority="1">foobar</attribute:b>[baz]</container:p>',
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p><attribute:b view-priority="1">foobar{baz</attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'merges wrapped nodes #4', () => {
 		it( 'merges wrapped nodes #4', () => {
 			test(
 			test(
-				'<container:p>[foo<attribute:i:1>bar</attribute:i:1>]baz</container:p>',
-				'<attribute:b:1></attribute:b:1>',
-				'<container:p>[<attribute:b:1>foo<attribute:i:1>bar</attribute:i:1></attribute:b:1>]baz</container:p>'
+				'<container:p>[foo<attribute:i view-priority="1">bar</attribute:i>]baz</container:p>',
+				'<attribute:b view-priority="1"></attribute:b>',
+				'<container:p>' +
+					'[<attribute:b view-priority="1">foo<attribute:i view-priority="1">bar</attribute:i></attribute:b>]baz' +
+				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'merges wrapped nodes #5', () => {
 		it( 'merges wrapped nodes #5', () => {
 			test(
 			test(
-				'<container:p>[foo<attribute:i:1>bar</attribute:i:1>baz]</container:p>',
-				'<attribute:b:2></attribute:b:2>',
+				'<container:p>[foo<attribute:i view-priority="1">bar</attribute:i>baz]</container:p>',
+				'<attribute:b view-priority="2"></attribute:b>',
 				'<container:p>' +
 				'<container:p>' +
 				'[' +
 				'[' +
-					'<attribute:b:2>foo</attribute:b:2>' +
-					'<attribute:i:1>' +
-						'<attribute:b:2>bar</attribute:b:2>' +
-					'</attribute:i:1>' +
-					'<attribute:b:2>baz</attribute:b:2>' +
+					'<attribute:b view-priority="2">foo</attribute:b>' +
+					'<attribute:i view-priority="1">' +
+						'<attribute:b view-priority="2">bar</attribute:b>' +
+					'</attribute:i>' +
+					'<attribute:b view-priority="2">baz</attribute:b>' +
 				']' +
 				']' +
 				'</container:p>'
 				'</container:p>'
 			);
 			);
@@ -189,45 +195,47 @@ describe( 'writer', () => {
 
 
 		it( 'should wrap single element by merging attributes', () => {
 		it( 'should wrap single element by merging attributes', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1 foo="bar" one="two"></attribute:b:1>]</container:p>',
-				'<attribute:b:1 baz="qux" one="two"></attribute:b:1>',
-				'<container:p>[<attribute:b:1 baz="qux" foo="bar" one="two"></attribute:b:1>]</container:p>'
+				'<container:p>[<attribute:b view-priority="1" foo="bar" one="two"></attribute:b>]</container:p>',
+				'<attribute:b view-priority="1" baz="qux" one="two"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1" baz="qux" foo="bar" one="two"></attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should not merge attributes when they differ', () => {
 		it( 'should not merge attributes when they differ', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1 foo="bar"></attribute:b:1>]</container:p>',
-				'<attribute:b:1 foo="baz"></attribute:b:1>',
-				'<container:p>[<attribute:b:1 foo="baz"><attribute:b:1 foo="bar"></attribute:b:1></attribute:b:1>]</container:p>'
+				'<container:p>[<attribute:b view-priority="1" foo="bar"></attribute:b>]</container:p>',
+				'<attribute:b view-priority="1" foo="baz"></attribute:b>',
+				'<container:p>' +
+					'[<attribute:b view-priority="1" foo="baz"><attribute:b view-priority="1" foo="bar"></attribute:b></attribute:b>]' +
+				'</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should wrap single element by merging classes', () => {
 		it( 'should wrap single element by merging classes', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1 class="foo bar baz"></attribute:b:1>]</container:p>',
-				'<attribute:b:1 class="foo bar qux jax"></attribute:b:1>',
-				'<container:p>[<attribute:b:1 class="foo bar baz qux jax"></attribute:b:1>]</container:p>'
+				'<container:p>[<attribute:b view-priority="1" class="foo bar baz"></attribute:b>]</container:p>',
+				'<attribute:b view-priority="1" class="foo bar qux jax"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1" class="foo bar baz qux jax"></attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should wrap single element by merging styles', () => {
 		it( 'should wrap single element by merging styles', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1 style="color:red; position: absolute;"></attribute:b:1>]</container:p>',
-				'<attribute:b:1 style="color:red; top: 20px;"></attribute:b:1>',
-				'<container:p>[<attribute:b:1 style="color:red;position:absolute;top:20px;"></attribute:b:1>]</container:p>'
+				'<container:p>[<attribute:b view-priority="1" style="color:red; position: absolute;"></attribute:b>]</container:p>',
+				'<attribute:b view-priority="1" style="color:red; top: 20px;"></attribute:b>',
+				'<container:p>[<attribute:b view-priority="1" style="color:red;position:absolute;top:20px;"></attribute:b>]</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should not merge styles when they differ', () => {
 		it( 'should not merge styles when they differ', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:1 style="color:red;"></attribute:b:1>]</container:p>',
-				'<attribute:b:1 style="color:black;"></attribute:b:1>',
+				'<container:p>[<attribute:b view-priority="1" style="color:red;"></attribute:b>]</container:p>',
+				'<attribute:b view-priority="1" style="color:black;"></attribute:b>',
 				'<container:p>' +
 				'<container:p>' +
 				'[' +
 				'[' +
-					'<attribute:b:1 style="color:black;">' +
-						'<attribute:b:1 style="color:red;"></attribute:b:1>' +
-					'</attribute:b:1>' +
+					'<attribute:b view-priority="1" style="color:black;">' +
+						'<attribute:b view-priority="1" style="color:red;"></attribute:b>' +
+					'</attribute:b>' +
 				']' +
 				']' +
 				'</container:p>'
 				'</container:p>'
 			);
 			);
@@ -235,27 +243,29 @@ describe( 'writer', () => {
 
 
 		it( 'should not merge single elements when they have different priority', () => {
 		it( 'should not merge single elements when they have different priority', () => {
 			test(
 			test(
-				'<container:p>[<attribute:b:2 style="color:red;"></attribute:b:2>]</container:p>',
-				'<attribute:b:1 style="color:red;"></attribute:b:1>',
+				'<container:p>[<attribute:b view-priority="2" style="color:red;"></attribute:b>]</container:p>',
+				'<attribute:b view-priority="1" style="color:red;"></attribute:b>',
 				'<container:p>' +
 				'<container:p>' +
 				'[' +
 				'[' +
-					'<attribute:b:1 style="color:red;">' +
-						'<attribute:b:2 style="color:red;"></attribute:b:2>' +
-					'</attribute:b:1>' +
+					'<attribute:b view-priority="1" style="color:red;">' +
+						'<attribute:b view-priority="2" style="color:red;"></attribute:b>' +
+					'</attribute:b>' +
 				']</container:p>'
 				']</container:p>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should be merged with outside element when wrapping all children', () => {
 		it( 'should be merged with outside element when wrapping all children', () => {
 			test(
 			test(
-				'<container:p><attribute:b:1 foo="bar">[foobar<attribute:i:1>baz</attribute:i:1>]</attribute:b:1></container:p>',
-				'<attribute:b:1 baz="qux"></attribute:b:1>',
+				'<container:p>' +
+					'<attribute:b view-priority="1" foo="bar">[foobar<attribute:i view-priority="1">baz</attribute:i>]</attribute:b>' +
+				'</container:p>',
+				'<attribute:b view-priority="1" baz="qux"></attribute:b>',
 				'<container:p>' +
 				'<container:p>' +
 				'[' +
 				'[' +
-					'<attribute:b:1 baz="qux" foo="bar">' +
+					'<attribute:b view-priority="1" baz="qux" foo="bar">' +
 						'foobar' +
 						'foobar' +
-						'<attribute:i:1>baz</attribute:i:1>' +
-					'</attribute:b:1>' +
+						'<attribute:i view-priority="1">baz</attribute:i>' +
+					'</attribute:b>' +
 				']' +
 				']' +
 				'</container:p>'
 				'</container:p>'
 			);
 			);

+ 29 - 28
packages/ckeditor5-engine/tests/view/writer/wrapposition.js

@@ -41,84 +41,85 @@ describe( 'wrapPosition', () => {
 	it( 'should wrap position at the beginning of text node', () => {
 	it( 'should wrap position at the beginning of text node', () => {
 		test(
 		test(
 			'<container:p>{}foobar</container:p>',
 			'<container:p>{}foobar</container:p>',
-			'<attribute:b:1></attribute:b:1>',
-			'<container:p><attribute:b:1>[]</attribute:b:1>foobar</container:p>'
+			'<attribute:b view-priority="1"></attribute:b>',
+			'<container:p><attribute:b view-priority="1">[]</attribute:b>foobar</container:p>'
 		);
 		);
 	} );
 	} );
 
 
 	it( 'should wrap position inside text node', () => {
 	it( 'should wrap position inside text node', () => {
 		test(
 		test(
 			'<container:p>foo{}bar</container:p>',
 			'<container:p>foo{}bar</container:p>',
-			'<attribute:b:1></attribute:b:1>',
-			'<container:p>foo<attribute:b:1>[]</attribute:b:1>bar</container:p>'
+			'<attribute:b view-priority="1"></attribute:b>',
+			'<container:p>foo<attribute:b view-priority="1">[]</attribute:b>bar</container:p>'
 		);
 		);
 	} );
 	} );
 
 
 	it( 'should support unicode', () => {
 	it( 'should support unicode', () => {
 		test(
 		test(
 			'<container:p>நிலை{}க்கு</container:p>',
 			'<container:p>நிலை{}க்கு</container:p>',
-			'<attribute:b:1></attribute:b:1>',
-			'<container:p>நிலை<attribute:b:1>[]</attribute:b:1>க்கு</container:p>'
+			'<attribute:b view-priority="1"></attribute:b>',
+			'<container:p>நிலை<attribute:b view-priority="1">[]</attribute:b>க்கு</container:p>'
 		);
 		);
 	} );
 	} );
 
 
 	it( 'should wrap position inside document fragment', () => {
 	it( 'should wrap position inside document fragment', () => {
 		test(
 		test(
-			'<attribute:b:1>foo</attribute:b:1>[]<attribute:b:3>bar</attribute:b:3>',
-			'<attribute:b:2></attribute:b:2>',
-			'<attribute:b:1>foo</attribute:b:1><attribute:b:2>[]</attribute:b:2><attribute:b:3>bar</attribute:b:3>'
+			'<attribute:b view-priority="1">foo</attribute:b>[]<attribute:b view-priority="3">bar</attribute:b>',
+			'<attribute:b view-priority="2"></attribute:b>',
+			'<attribute:b view-priority="1">foo</attribute:b><attribute:b view-priority="2">[]</attribute:b>' +
+			'<attribute:b view-priority="3">bar</attribute:b>'
 		);
 		);
 	} );
 	} );
 
 
 	it( 'should wrap position at the end of text node', () => {
 	it( 'should wrap position at the end of text node', () => {
 		test(
 		test(
 			'<container:p>foobar{}</container:p>',
 			'<container:p>foobar{}</container:p>',
-			'<attribute:b:1></attribute:b:1>',
-			'<container:p>foobar<attribute:b:1>[]</attribute:b:1></container:p>'
+			'<attribute:b view-priority="1"></attribute:b>',
+			'<container:p>foobar<attribute:b view-priority="1">[]</attribute:b></container:p>'
 		);
 		);
 	} );
 	} );
 
 
 	it( 'should merge with existing attributes #1', () => {
 	it( 'should merge with existing attributes #1', () => {
 		test(
 		test(
-			'<container:p><attribute:b:1>foo</attribute:b:1>[]</container:p>',
-			'<attribute:b:1></attribute:b:1>',
-			'<container:p><attribute:b:1>foo{}</attribute:b:1></container:p>'
+			'<container:p><attribute:b view-priority="1">foo</attribute:b>[]</container:p>',
+			'<attribute:b view-priority="1"></attribute:b>',
+			'<container:p><attribute:b view-priority="1">foo{}</attribute:b></container:p>'
 		);
 		);
 	} );
 	} );
 
 
 	it( 'should merge with existing attributes #2', () => {
 	it( 'should merge with existing attributes #2', () => {
 		test(
 		test(
-			'<container:p>[]<attribute:b:1>foo</attribute:b:1></container:p>',
-			'<attribute:b:1></attribute:b:1>',
-			'<container:p><attribute:b:1>{}foo</attribute:b:1></container:p>'
+			'<container:p>[]<attribute:b view-priority="1">foo</attribute:b></container:p>',
+			'<attribute:b view-priority="1"></attribute:b>',
+			'<container:p><attribute:b view-priority="1">{}foo</attribute:b></container:p>'
 		);
 		);
 	} );
 	} );
 
 
 	it( 'should wrap when inside nested attributes', () => {
 	it( 'should wrap when inside nested attributes', () => {
 		test(
 		test(
-			'<container:p><attribute:b:1>foo{}bar</attribute:b:1></container:p>',
-			'<attribute:u:1></attribute:u:1>',
+			'<container:p><attribute:b view-priority="1">foo{}bar</attribute:b></container:p>',
+			'<attribute:u view-priority="1"></attribute:u>',
 			'<container:p>' +
 			'<container:p>' +
-				'<attribute:b:1>foo</attribute:b:1>' +
-				'<attribute:u:1><attribute:b:1>[]</attribute:b:1></attribute:u:1>' +
-				'<attribute:b:1>bar</attribute:b:1>' +
+				'<attribute:b view-priority="1">foo</attribute:b>' +
+				'<attribute:u view-priority="1"><attribute:b view-priority="1">[]</attribute:b></attribute:u>' +
+				'<attribute:b view-priority="1">bar</attribute:b>' +
 			'</container:p>'
 			'</container:p>'
 		);
 		);
 	} );
 	} );
 
 
 	it( 'should merge when wrapping between same attribute', () => {
 	it( 'should merge when wrapping between same attribute', () => {
 		test(
 		test(
-			'<container:p><attribute:b:1>foo</attribute:b:1>[]<attribute:b:1>bar</attribute:b:1></container:p>',
-			'<attribute:b:1></attribute:b:1>',
-			'<container:p><attribute:b:1>foo{}bar</attribute:b:1></container:p>'
+			'<container:p><attribute:b view-priority="1">foo</attribute:b>[]<attribute:b view-priority="1">bar</attribute:b></container:p>',
+			'<attribute:b view-priority="1"></attribute:b>',
+			'<container:p><attribute:b view-priority="1">foo{}bar</attribute:b></container:p>'
 		);
 		);
 	} );
 	} );
 
 
 	it( 'should move position to text node if in same attribute', () => {
 	it( 'should move position to text node if in same attribute', () => {
 		test(
 		test(
-			'<container:p><attribute:b:1>foobar[]</attribute:b:1></container:p>',
-			'<attribute:b:1></attribute:b:1>',
-			'<container:p><attribute:b:1>foobar{}</attribute:b:1></container:p>'
+			'<container:p><attribute:b view-priority="1">foobar[]</attribute:b></container:p>',
+			'<attribute:b view-priority="1"></attribute:b>',
+			'<container:p><attribute:b view-priority="1">foobar{}</attribute:b></container:p>'
 		);
 		);
 	} );
 	} );
 } );
 } );