浏览代码

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 ?
 				elementCreator.clone( true ) :
-				elementCreator( data.value, selection, consumable, conversionApi );
+				elementCreator( data.value, data, selection, consumable, conversionApi );
 
 		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
  * {@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.
+ * When provided `Function` does not return element, then will be no conversion.
  *
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * {@link engine.conversion.ModelConversionDispatcher}).
@@ -209,25 +210,27 @@ export function removeAttribute( attributeCreator ) {
  */
 export function wrap( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
-		consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
-
 		let viewRange = conversionApi.mapper.toViewRange( data.range );
 
 		const viewElement = ( elementCreator instanceof ViewElement ) ?
 			elementCreator.clone( true ) :
 			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' );
  *
  *			// 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.
  *			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.conversion.ModelConversionDispatcher model to view} and
- * {@link engine.conversion.ViewConversionDispatcher view to model} converters.
+ * * {@link engine.conversion.ViewConversionDispatcher view to model} converters.
  *
  * @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 Range from '/ckeditor5/engine/model/range.js';
 import Position from '/ckeditor5/engine/model/position.js';
+import count from '/ckeditor5/utils/count.js';
 
 describe( 'model test utils', () => {
 	let document, root, selection, sandbox;
@@ -20,6 +21,15 @@ describe( 'model test utils', () => {
 		selection = document.selection;
 		sandbox = sinon.sandbox.create();
 		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( () => {
@@ -41,7 +51,7 @@ describe( 'model test utils', () => {
 			root.appendChildren( new Element( 'b', null, new Text( 'btext' ) ) );
 			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.calledWithExactly( stringifySpy, root, document.selection );
 		} );
@@ -50,7 +60,7 @@ describe( 'model test utils', () => {
 			root.appendChildren( 'நிலைக்கு' );
 			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', () => {
@@ -65,7 +75,6 @@ describe( 'model test utils', () => {
 			const parseSpy = sandbox.spy( setData, '_parse' );
 			const options = {};
 			const data = '<b>btext</b>text';
-			document.schema.registerItem( 'b', '$inline' );
 
 			setData( document, data, options );
 
@@ -78,8 +87,7 @@ describe( 'model test utils', () => {
 		it( 'should use parse method with selection', () => {
 			const parseSpy = sandbox.spy( setData, '_parse' );
 			const options = {};
-			const data = '<selection><b>btext</b></selection>';
-			document.schema.registerItem( 'b', '$inline' );
+			const data = '[<b>btext</b>]';
 
 			setData( document, data, options );
 
@@ -90,52 +98,50 @@ describe( 'model test utils', () => {
 		} );
 
 		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', () => {
-			test( '<selection>this is test text</selection>' );
+			test( '[this is test text]' );
 		} );
 
 		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', () => {
-			test( '<selection>this is test</selection> text' );
+			test( '[this is test] text' );
 		} );
 
 		it( 'should insert text with selection inside #2', () => {
-			test( 'this is <selection>test text</selection>' );
+			test( 'this is [test text]' );
 		} );
 
 		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', () => {
-			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', () => {
-			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', () => {
-			document.schema.registerItem( 'b', '$inline' );
-			test( '<b><selection>foo bar</b></selection>' );
+			test( '<b>[foo bar</b>]' );
 		} );
 
 		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', () => {
-			test( 'நி<selection>லைக்</selection>கு' );
+			test( 'நி[லைக்]கு' );
 		} );
 
 		it( 'should throw an error when passing invalid document', () => {
@@ -155,7 +161,7 @@ describe( 'model test utils', () => {
 	describe( 'stringify', () => {
 		it( 'should stringify text', () => {
 			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', () => {
@@ -195,14 +201,14 @@ describe( 'model test utils', () => {
 		it( 'writes element attributes', () => {
 			root.appendChildren(
 				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
 			// to be compared in the tests with some patterns.
 			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(
-				'<$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 );
 
 				expect( stringify( root, selection ) ).to.equal(
-					'<selection />'
+					'[]'
 				);
 			} );
 
@@ -256,7 +261,7 @@ describe( 'model test utils', () => {
 				selection.collapse( root );
 
 				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 );
 
 				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' );
 
 				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' );
 
 				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', () => {
@@ -291,16 +298,7 @@ describe( 'model test utils', () => {
 				selection.clearAttributes();
 
 				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 );
 
 				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', () => {
@@ -318,7 +318,7 @@ describe( 'model test utils', () => {
 				);
 
 				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(
-					'<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(
-					'<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(
 					Range.createFromParentsAndOffsets( elA, 0, elB, 0 ),
 					true
 				);
 
 				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 );
 
 				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 ] );
 
 				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', {
-			data: '<selection />',
+			data: '[]',
 			check( fragment, selection ) {
 				expect( fragment ).to.be.instanceOf( DocumentFragment );
 				expect( fragment.childCount ).to.equal( 0 );
@@ -390,7 +390,7 @@ describe( 'model test utils', () => {
 		} );
 
 		test( 'returns Element if range is around single element', {
-			data: '<selection><a></a></selection>',
+			data: '[<a></a>]',
 			check( el, selection ) {
 				const fragment = el.parent;
 				expect( el ).to.be.instanceOf( Element );
@@ -417,29 +417,24 @@ describe( 'model test utils', () => {
 		} );
 
 		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 ) {
+				expect( a.getAttribute( 'bar' ) ).to.equal( 'true' );
 				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', {
-			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 ) {
 				expect( root.childCount ).to.equal( 3 );
 				expect( root.maxOffset ).to.equal( 9 );
 				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 ).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( () => {
 				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', () => {
 			test( 'sets collapsed selection in an element', {
-				data: '<a><selection /></a>',
+				data: '<a>[]</a>',
 				check( root, selection ) {
 					expect( selection.getFirstPosition().parent ).to.have.property( 'name', 'a' );
 				}
 			} );
 
 			test( 'sets collapsed selection between elements', {
-				data: '<a></a><selection /><b></b>'
+				data: '<a></a>[]<b></b>'
 			} );
 
 			test( 'sets collapsed selection before a text', {
-				data: '<a></a><selection />foo'
+				data: '<a></a>[]foo'
 			} );
 
 			test( 'sets collapsed selection after a text', {
-				data: 'foo<selection />'
+				data: 'foo[]'
 			} );
 
 			test( 'sets collapsed selection within a text', {
-				data: 'foo<selection />bar',
+				data: 'foo[]bar',
 				check( text, selection ) {
 					expect( text.offsetSize ).to.equal( 6 );
 					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', {
-				data: 'foo<selection /><$text bold=true>bar</$text>',
+				data: 'foo[]<$text bold="true">bar</$text>',
 				check( root, selection ) {
 					expect( root.maxOffset ).to.equal( 6 );
 					expect( selection.getAttribute( 'bold' ) ).to.be.undefined;
@@ -539,42 +506,57 @@ describe( 'model test utils', () => {
 			} );
 
 			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', {
-				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', () => {
 				expect( () => {
-					parse( 'foo</selection>' );
-				} ).to.throw( Error, 'Parse error - missing selection start.' );
+					parse( 'foo]' );
+				} ).to.throw( Error );
 			} );
 
 			it( 'throws when missing selection end', () => {
 				expect( () => {
-					parse( '<selection>foo' );
-				} ).to.throw( Error, 'Parse error - missing selection end.' );
+					parse( '[foo' );
+				} ).to.throw( Error );
 			} );
 		} );
 
 		function test( title, options ) {
 			it( title, () => {
 				const output = options.output || options.data;
-				const data = parse( options.data );
+				const data = parse( options.data, document.schema );
 				let model, selection;
 
 				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>' );
 		} );
 
+		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', () => {
 			const text = new Text( 'foobar' );
 			const p = new Element( 'p', null, text );
@@ -228,7 +241,7 @@ describe( 'view test utils', () => {
 			const p = new ContainerElement( 'p', null, b );
 
 			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', () => {
@@ -400,10 +413,10 @@ describe( 'view test utils', () => {
 		} );
 
 		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' );
 			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' );
 			attribute2.priority = 44;
 
@@ -413,7 +426,7 @@ describe( 'view test utils', () => {
 		} );
 
 		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.childCount ).to.equal( 2 );
 			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.
  */
 
-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 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.
  *
+ * ** 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 {Object} [options]
  * @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.
  */
 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.' );
 	}
 
@@ -44,39 +63,53 @@ getData._stringify = stringify;
  * Sets the contents of the {@link engine.model.Document Document} provided as HTML-like string.
  * 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 {String} data HTML-like string to write into Document.
  * @param {Object} options
  * @param {String} [options.rootName='main'] Root name where parsed data will be stored. If not provided, default `main`
  * 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}.
  */
 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.' );
 	}
 
-	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 {
-		model = result;
+		modelDocumentFragment = parsedResult;
 	}
 
-	// Save to model.
-	const modelRoot = document.getRoot( options.rootName || 'main' );
-
 	document.enqueueChanges( () => {
+		// Replace existing model in document by new one.
 		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 ) {
 			const ranges = [];
 
@@ -86,22 +119,23 @@ export function setData( document, data, options = {} ) {
 				// 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
 				// 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 {
-					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 {
-					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.setAttributesTo( selection.getAttributes() );
 		}
 	} );
 }
@@ -112,393 +146,231 @@ setData._parse = parse;
 /**
  * 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|
  * 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
  * converted to selection containing this range. If Position instance is provided - it will be converted to selection
  * containing one range collapsed at this position.
  * @returns {String} HTML-like string representing the model.
  */
 export function stringify( node, selectionOrPositionOrRange = null ) {
+	const mapper = new Mapper();
 	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 {
 		// Node is detached - create new document fragment.
 		if ( !node.parent ) {
-			const fragment = new DocumentFragment( node );
-			range = Range.createIn( fragment );
+			const fragment = new ModelDocumentFragment( node );
+			range = ModelRange.createIn( fragment );
 		} 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;
-	} else if ( selectionOrPositionOrRange instanceof Range ) {
-		selection = new Selection();
+	} else if ( selectionOrPositionOrRange instanceof ModelRange ) {
+		selection = new ModelSelection();
 		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}.
  *
+ * ** 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 {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
  * 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 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 Selection from '/ckeditor5/engine/view/selection.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>`
  * instead of `<p>` and `<attribute:b>` instead of `<b>`).
  * @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.
  */
 export function getData( document, options = {} ) {
@@ -125,6 +125,10 @@ setData._parse = parse;
  *
  *		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:
  *
  *		const text = new Text( 'foobar' );
@@ -161,7 +165,7 @@ setData._parse = parse;
  *
  *		const attribute = new AttributeElement( 'b' );
  *		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.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>`
  * instead of `<p>` and `<attribute:b>` instead of `<b>`).
  * @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.
  * 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.
  */
 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>' );
  *
+ * ** 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
  * 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
  * {@link engine.view.Element Element} or {@link engine.view.DocumentFragment DocumentFragment} - this node
  * 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
  * or object with two fields `view` and `selection` when selection ranges were included in data to parse.
  */
 export function parse( data, options = {} ) {
 	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.
 	let view = processor.toView( data );
@@ -314,6 +330,18 @@ export function parse( data, options = {} ) {
  * @private
  */
 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}.
 	 * 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 != ranges.length ) {
 				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( {
 					bracket: bracket,
-					textOffset: index - offset
+					textOffset: index - offset,
+					outer: index === 0 || index == node._data.length - 1
 				} );
 
 				offset++;
 			}
+
 			text = text.replace( regexp, '' );
 			node.data = text;
 			const index = node.index;
@@ -401,7 +431,10 @@ class RangeParser {
 			for ( let item of brackets ) {
 				// Non-empty text node.
 				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.
 						this._positions.push( {
 							bracket: item.bracket,
@@ -409,7 +442,7 @@ class RangeParser {
 						} );
 					} else {
 						// 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.` );
 						}
 
@@ -423,7 +456,7 @@ class RangeParser {
 						} );
 					}
 				} 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. ` );
 					}
 
@@ -520,6 +553,8 @@ class ViewStringify {
 	 * @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
 	 * 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 = {} ) {
 		this.root = root;
@@ -533,6 +568,7 @@ class ViewStringify {
 		this.showType = !!options.showType;
 		this.showPriority = !!options.showPriority;
 		this.ignoreRoot = !!options.ignoreRoot;
+		this.sameSelectionCharacters = !!options.sameSelectionCharacters;
 	}
 
 	/**
@@ -626,6 +662,15 @@ class ViewStringify {
 	_stringifyTextRanges( node ) {
 		const length = node.data.length;
 		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.
 		result[ length ] = '';
@@ -646,14 +691,14 @@ class ViewStringify {
 
 			if ( start.parent == node && start.offset >= 0 && start.offset <= length ) {
 				if ( range.isCollapsed ) {
-					result[ end.offset ].collapsed += TEXT_RANGE_START_TOKEN + TEXT_RANGE_END_TOKEN;
+					result[ end.offset ].collapsed += rangeStartToken + rangeEndToken;
 				} 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  ) {
-				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.
 	 * 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">`).
 	 *
 	 * @private
@@ -672,17 +717,18 @@ class ViewStringify {
 	 */
 	_stringifyElementOpen( element ) {
 		const priority = this._stringifyElementPriority( 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 parts = [ name, attributes ];
+		const parts = [ name, priority, attributes ];
 
 		return `<${ parts.filter( i => i !== '' ).join( ' ' ) }>`;
 	}
 
 	/**
 	 * 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>`).
 	 *
 	 * @private
@@ -690,9 +736,8 @@ class ViewStringify {
 	 * @returns {String}
 	 */
 	_stringifyElementClose( element ) {
-		const priority = this._stringifyElementPriority( 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 }>`;
 	}
@@ -733,7 +778,7 @@ class ViewStringify {
 	 */
 	_stringifyElementPriority( element ) {
 		if ( this.showPriority && element instanceof AttributeElement ) {
-			return element.priority;
+			return `view-priority="${ element.priority }"`;
 		}
 
 		return '';
@@ -786,19 +831,19 @@ function _convertViewElements( rootNode ) {
 
 // Converts {@link engine.view.Element Element} to {@link engine.view.AttributeElement AttributeElement} or
 // {@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.
 // If element's name is in format `container:p` - it will be converted to
 // {@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.
 //
 // @param {engine.view.Element} viewElement View element to convert.
 // @returns {engine.view.Element|engine.view.AttributeElement|engine.view.ContainerElement} Tree view
 // element converted according to it's name.
 function _convertElement( viewElement ) {
-	const info = _convertElementName( viewElement );
 	let newElement;
+	const info = _convertElementNameAndPriority( viewElement );
 
 	if ( info.type == 'attribute' ) {
 		newElement = new AttributeElement( info.name );
@@ -820,24 +865,26 @@ function _convertElement( viewElement ) {
 	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.
-// 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.
 // @returns {Object} info Object with parsed information.
 // @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 {Number|null} info.priority Parsed priority of the element.
-function _convertElementName( viewElement ) {
+function _convertElementNameAndPriority( viewElement ) {
 	const parts = viewElement.name.split( ':' );
+	const priority = _convertPriority( viewElement.getAttribute( 'view-priority' ) );
+	viewElement.removeAttribute( 'view-priority' );
 
 	if ( parts.length == 1 ) {
 		return {
 			name: parts[ 0 ],
-			type: null,
-			priority: null
+			type: priority !== null ? 'attribute' : null,
+			priority: priority
 		};
 	}
 
@@ -849,17 +896,6 @@ function _convertElementName( viewElement ) {
 			return {
 				name: parts[ 1 ],
 				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
 			};
 		}
@@ -867,20 +903,6 @@ function _convertElementName( viewElement ) {
 		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 }.` );
 }
 

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

@@ -42,6 +42,8 @@ beforeEach( () => {
 	modelRoot = modelDoc.createRoot();
 	modelSelection = modelDoc.selection;
 
+	modelDoc.schema.allow( { name: '$text', inside: '$root' } );
+
 	viewDoc = new ViewDocument();
 	viewRoot = viewDoc.createRoot( 'div' );
 	viewSelection = viewDoc.selection;
@@ -87,7 +89,7 @@ describe( 'default converters', () => {
 		it( 'in same container, over attribute', () => {
 			test(
 				[ 1, 5 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'f{o<strong>ob</strong>a}r'
 			);
 		} );
@@ -95,7 +97,7 @@ describe( 'default converters', () => {
 		it( 'in same container, next to attribute', () => {
 			test(
 				[ 1, 2 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'f{o}<strong>ob</strong>ar'
 			);
 		} );
@@ -103,7 +105,7 @@ describe( 'default converters', () => {
 		it( 'in same attribute', () => {
 			test(
 				[ 2, 4 ],
-				'f<$text bold=true>ooba</$text>r',
+				'f<$text bold="true">ooba</$text>r',
 				'f<strong>o{ob}a</strong>r'
 			);
 		} );
@@ -111,7 +113,7 @@ describe( 'default converters', () => {
 		it( 'in same attribute, selection same as attribute', () => {
 			test(
 				[ 2, 4 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'fo{<strong>ob</strong>}ar'
 			);
 		} );
@@ -119,7 +121,7 @@ describe( 'default converters', () => {
 		it( 'starts in text node, ends in attribute #1', () => {
 			test(
 				[ 1, 3 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'f{o<strong>o}b</strong>ar'
 			);
 		} );
@@ -127,7 +129,7 @@ describe( 'default converters', () => {
 		it( 'starts in text node, ends in attribute #2', () => {
 			test(
 				[ 1, 4 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'f{o<strong>ob</strong>}ar'
 			);
 		} );
@@ -135,7 +137,7 @@ describe( 'default converters', () => {
 		it( 'starts in attribute, ends in text node', () => {
 			test(
 				[ 3, 5 ],
-				'fo<$text bold=true>ob</$text>ar',
+				'fo<$text bold="true">ob</$text>ar',
 				'fo<strong>o{b</strong>a}r'
 			);
 		} );
@@ -178,7 +180,7 @@ describe( 'default converters', () => {
 		it( 'in attribute', () => {
 			test(
 				[ 3, 3 ],
-				'f<$text bold=true>ooba</$text>r',
+				'f<$text bold="true">ooba</$text>r',
 				'f<strong>oo{}ba</strong>r'
 			);
 		} );
@@ -195,7 +197,7 @@ describe( 'default converters', () => {
 		it( 'in attribute with extra attributes', () => {
 			test(
 				[ 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',
 				{ italic: true }
 			);
@@ -215,7 +217,7 @@ describe( 'default converters', () => {
 			// Similar test case as above
 			test(
 				[ 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.
 			);
 		} );
@@ -311,16 +313,16 @@ describe( 'clean-up', () => {
 
 describe( 'using element creator for attributes conversion', () => {
 	beforeEach( () => {
-		function styleElementCreator( styleValue ) {
-			if ( styleValue == 'important' ) {
+		function themeElementCreator( themeValue ) {
+			if ( themeValue == 'important' ) {
 				return new ViewAttributeElement( 'strong', { style: 'text-transform:uppercase;' } );
-			} else if ( styleValue == 'gold' ) {
+			} else if ( themeValue == 'gold' ) {
 				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' ) ) );
 	} );
@@ -329,7 +331,7 @@ describe( 'using element creator for attributes conversion', () => {
 		it( 'in same container, over attribute', () => {
 			test(
 				[ 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'
 			);
 		} );
@@ -337,7 +339,7 @@ describe( 'using element creator for attributes conversion', () => {
 		it( 'in same attribute', () => {
 			test(
 				[ 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'
 			);
 		} );
@@ -345,7 +347,7 @@ describe( 'using element creator for attributes conversion', () => {
 		it( 'in same attribute, selection same as attribute', () => {
 			test(
 				[ 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'
 			);
 		} );
@@ -353,7 +355,7 @@ describe( 'using element creator for attributes conversion', () => {
 		it( 'starts in attribute, ends in text node', () => {
 			test(
 				[ 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'
 			);
 		} );
@@ -363,24 +365,24 @@ describe( 'using element creator for attributes conversion', () => {
 		it( 'in attribute', () => {
 			test(
 				[ 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'
 			);
 		} );
 
-		it( 'in container with style attribute', () => {
+		it( 'in container with theme attribute', () => {
 			test(
 				[ 1, 1 ],
 				'foobar',
 				'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(
 				[ 3, 3 ],
-				'f<$text style="gold">ooba</$text>r',
+				'f<$text theme="gold">ooba</$text>r',
 				'f<span style="color:yellow;">oo</span>' +
 				'<em><span style="color:yellow;">[]</span></em>' +
 				'<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.
 			// 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.
 			// In example above, selection has both style and italic attribute.
 			test(
 				[ 3, 3 ],
-				'f<$text style="gold">ooba</$text>r',
+				'f<$text theme="gold">ooba</$text>r',
 				'f<span style="color:yellow;">oo</span>' +
 				'<strong style="text-transform:uppercase;">[]</strong>' +
 				'<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', () => {
 	beforeEach( () => {
+		modelDoc.schema.registerItem( 'table', '$block' );
+		modelDoc.schema.registerItem( 'tr', '$block' );
+		modelDoc.schema.registerItem( 'td', '$block' );
+
 		// "Universal" converter to convert table structure.
 		const tableConverter = insertElement( ( data ) => new ViewContainerElement( data.item.name ) );
 		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', () => {
 		test(
 			[ 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'
 		);
 	} );
 
 	it( 'should add a class to the selected table cell', () => {
-		modelDoc.schema.registerItem( 'table', '$block' );
 		test(
 			// table tr#0 |td#0, table tr#0 td#0|
 			[ [ 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', () => {
-		modelDoc.schema.registerItem( 'table', '$block' );
 		test(
 			// table tr td#1, table tr#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 } );
 
-		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', () => {
@@ -64,7 +65,7 @@ describe( 'convertSelectionChange', () => {
 
 		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', () => {
@@ -76,9 +77,8 @@ describe( 'convertSelectionChange', () => {
 
 		convertSelection( null, { newSelection: viewSelection } );
 
-		// Too bad getData shows only the first range.
 		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() );
 		expect( ranges.length ).to.equal( 2 );
@@ -98,11 +98,12 @@ describe( 'convertSelectionChange', () => {
 		const viewSelection = new ViewSelection();
 		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
 			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 } );
 
-		// 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>' );
 
 			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', () => {
+			modelDocument.schema.allow( { name: '$text', inside: '$root' } );
 			setData( modelDocument, 'foo' );
 
 			expect( data.get() ).to.equal( 'foo' );
@@ -170,7 +171,7 @@ describe( 'DataController', () => {
 
 		it( 'should get paragraphs without bold', () => {
 			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' );
 
@@ -179,7 +180,7 @@ describe( 'DataController', () => {
 
 		it( 'should get paragraphs with bold', () => {
 			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 ).fromAttribute( 'bold' ).toElement( 'b' );
@@ -189,6 +190,8 @@ describe( 'DataController', () => {
 
 		it( 'should get root name as a parameter', () => {
 			modelDocument.schema.registerItem( 'paragraph', '$block' );
+			modelDocument.schema.allow( { name: '$text', inside: '$root' } );
+
 			setData( modelDocument, '<paragraph>foo</paragraph>', { rootName: 'main' } );
 			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(
 				'<paragraph>foo</paragraph>' +
 				'<paragraph></paragraph>' +
-				'<paragraph>bar</paragraph>'
+				'<paragraph>bar</paragraph>',
+				model.schema
 			)._children );
 
 			model.enqueueChanges( () => {
@@ -175,7 +176,7 @@ describe( 'EditingController', () => {
 					expect( getModelData( model ) ).to.equal(
 						'<paragraph>foo</paragraph>' +
 						'<paragraph></paragraph>' +
-						'<paragraph>b<selection>a</selection>r</paragraph>' );
+						'<paragraph>b[a]r</paragraph>' );
 					done();
 				} );
 			} );
@@ -247,7 +248,7 @@ describe( 'EditingController', () => {
 			editing.destroy();
 
 			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 );
 			} );
 

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

@@ -22,18 +22,18 @@ describe( 'Composer', () => {
 
 	describe( 'constructor', () => {
 		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();
 
 			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;
 		} );
 
 		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();
 
@@ -43,11 +43,11 @@ describe( 'Composer', () => {
 				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', () => {
-			setData( document, '<p>foo<selection />bar</p>' );
+			setData( document, '<p>foo[]bar</p>' );
 
 			composer.fire( 'modifySelection', {
 				selection: document.selection,
@@ -57,7 +57,8 @@ describe( 'Composer', () => {
 			} );
 
 			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.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: 'p', attributes: [ 'align' ] } );
 	} );
@@ -34,78 +36,100 @@ describe( 'Delete utils', () => {
 		describe( 'in simple scenarios', () => {
 			test(
 				'does nothing on collapsed selection',
-				'f<selection />oo',
-				'f<selection />oo'
+				'f[]oo',
+				'f[]oo'
 			);
 
 			test(
 				'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(
 				'deletes whole text',
-				'<selection>foo</selection>',
-				'<selection />'
+				'[foo]',
+				'[]'
 			);
 
 			test(
 				'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(
 				'deletes an element',
-				'x<selection><img></img></selection>y',
-				'x<selection />y'
+				'x[<img></img>]y',
+				'x[]y'
 			);
 
 			test(
 				'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(
 				'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 }
 			);
 		} );
 
 		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.
@@ -120,101 +144,102 @@ describe( 'Delete utils', () => {
 		describe( 'in multi-element scenarios', () => {
 			test(
 				'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 }
 			);
 
 			test(
 				'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 }
 			);
 
 			test(
 				'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(
 				'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 }
 			);
 
 			test(
 				'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 }
 			);
 
-			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(
 				'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 }
 			);
 
 			test(
 				'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 }
 			);
 
 			test(
 				'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 }
 			);
 
 			// For code coverage reasons.
 			test(
 				'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 }
 			);
 
 			// If you disagree with this case please read the notes before this section.
 			test(
 				'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 }
 			);
 
 			// If you disagree with this case please read the notes before this section.
 			test(
 				'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 }
 			);
 
 			test(
 				'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 }
 			);
 
 			test(
 				'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 }
 			);
 		} );

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

@@ -16,6 +16,9 @@ describe( 'Delete utils', () => {
 	beforeEach( () => {
 		document = new Document();
 		document.schema.registerItem( 'p', '$block' );
+		document.schema.registerItem( 'x', '$block' );
+		document.schema.registerItem( 'img', '$inline' );
+		document.schema.allow( { name: '$text', inside: '$root' } );
 		document.createRoot();
 	} );
 
@@ -24,309 +27,357 @@ describe( 'Delete utils', () => {
 			describe( 'within element', () => {
 				test(
 					'does nothing on empty content',
-					'<selection />',
-					'<selection />'
+					'[]',
+					'[]'
 				);
 
 				test(
 					'does nothing on empty content (with empty element)',
-					'<p><selection /></p>',
-					'<p><selection /></p>'
+					'<p>[]</p>',
+					'<p>[]</p>'
 				);
 
 				test(
 					'does nothing on empty content (backward)',
-					'<selection />',
-					'<selection />',
+					'[]',
+					'[]',
 					{ direction: 'backward' }
 				);
 
 				test(
 					'does nothing on root boundary',
-					'<p>foo<selection /></p>',
-					'<p>foo<selection /></p>'
+					'<p>foo[]</p>',
+					'<p>foo[]</p>'
 				);
 
 				test(
 					'does nothing on root boundary (backward)',
-					'<p><selection />foo</p>',
-					'<p><selection />foo</p>',
+					'<p>[]foo</p>',
+					'<p>[]foo</p>',
 					{ direction: 'backward' }
 				);
 
 				test(
 					'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(
 					'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(
 					'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(
 					'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' }
 				);
 
-				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(
 					'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(
 					'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(
 					'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(
 					'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(
 					'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(
 					'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', () => {
 				test(
 					'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(
 					'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(
 					'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(
 					'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(
 					'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(
 					'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(
 					'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' }
 				);
+
+				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', () => {
 			test(
 				'does nothing on empty content',
-				'<selection />',
-				'<selection />',
+				'[]',
+				'[]',
 				{ unit: 'codePoint' }
 			);
 
 			test(
 				'does nothing on empty content (with empty element)',
-				'<p><selection /></p>',
-				'<p><selection /></p>',
+				'<p>[]</p>',
+				'<p>[]</p>',
 				{ unit: 'codePoint' }
 			);
 
 			test(
 				'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' }
 			);
 
-			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(
 				'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' }
 			);
 
-			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(
 				'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' }
 			);
 
 			test(
 				'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' }
 			);
 
-			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', () => {
 			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', () => {
 			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', () => {
 			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 );
 			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', () => {
@@ -93,7 +93,7 @@ describe( 'writer', () => {
 			const range = Range.createFromParentsAndOffsets( root, 3, root, 6 );
 			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', () => {
@@ -112,7 +112,7 @@ describe( 'writer', () => {
 			const range = Range.createFromParentsAndOffsets( root, 3, root, 6 );
 			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', () => {
@@ -127,21 +127,21 @@ describe( 'writer', () => {
 			const range = Range.createFromParentsAndOffsets( root, 6, root, 8 );
 			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', () => {
 			const range = Range.createFromParentsAndOffsets( root, 6, root, 7 );
 			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', () => {
 			const range = Range.createFromParentsAndOffsets( root, 0, root, 3 );
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			test(
-				'<container:p><attribute:b:1><attribute:u:1>foo{}bar</attribute:u:1></attribute:b:1></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' +
-						'</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' +
-						'</attribute:u:1>' +
-					'</attribute:b:1>' +
+						'</attribute:u>' +
+					'</attribute:b>' +
 				'</container:p>'
 			);
 		} );
 
 		it( 'should move from end of the nested text node to the container', () => {
 			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', () => {
 			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', () => {
 			test(
 				'<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', () => {
 			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', () => {
 			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>' +
-					'<attribute:b:1>' +
+					'<attribute:b view-priority="1">' +
 						'foo' +
-					'</attribute:b:1>' +
+					'</attribute:b>' +
 					'[' +
-					'<attribute:b:2>' +
+					'<attribute:b view-priority="2">' +
 						'baz' +
-					'</attribute:b:2>' +
+					'</attribute:b>' +
 					']' +
-					'<attribute:b:1>' +
+					'<attribute:b view-priority="1">' +
 						'bar' +
-					'</attribute:b:1>' +
+					'</attribute:b>' +
 				'</container:p>'
 			);
 		} );
@@ -109,16 +109,16 @@ describe( 'writer', () => {
 		it( 'should allow to insert multiple nodes', () => {
 			test(
 				'<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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			test(
 				'<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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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'
 			);
 		} );
 
 		it( 'should merge after removing #2', () => {
 			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', () => {
 			test(
 				'<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>'
 			);
 		} );
@@ -42,7 +42,7 @@ describe( 'writer', () => {
 		it( 'should do nothing on single text node', () => {
 			test(
 				'<container:p>[foobar]</container:p>',
-				'<attribute:b:1></attribute:b:1>',
+				'<attribute:b view-priority="1"></attribute:b>',
 				'<container:p>[foobar]</container:p>'
 			);
 		} );
@@ -85,17 +85,17 @@ describe( 'writer', () => {
 
 		it( 'should unwrap single node', () => {
 			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>'
 			);
 		} );
 
 		it( 'should not unwrap attributes with different priorities #1', () => {
 			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(
 				'<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>',
-				'<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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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>'
 			);
 		} );
@@ -148,15 +148,15 @@ describe( 'writer', () => {
 		it( 'should merge unwrapped nodes #2', () => {
 			const input = '<container:p>' +
 			'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>';
-			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 );
 		} );
@@ -164,19 +164,19 @@ describe( 'writer', () => {
 		it( 'should merge unwrapped nodes #3', () => {
 			const input = '<container:p>' +
 				'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>';
-			const attribute = '<attribute:b:1></attribute:b:1>';
+			const attribute = '<attribute:b view-priority="1"></attribute:b>';
 			const result = '<container:p>' +
 				'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>';
 
 			test( input, attribute, result );
@@ -185,18 +185,18 @@ describe( 'writer', () => {
 		it( 'should merge unwrapped nodes #4', () => {
 			const input = '<container:p>' +
 				'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>';
-			const attribute = '<attribute:b:1></attribute:b:1>';
+			const attribute = '<attribute:b view-priority="1"></attribute:b>';
 			const result = '<container:p>' +
 				'foo' +
-				'<attribute:u:1>bar{baz}qux</attribute:u:1>' +
+				'<attribute:u view-priority="1">bar{baz}qux</attribute:u>' +
 			'</container:p>';
 
 			test( input, attribute, result );
@@ -205,13 +205,13 @@ describe( 'writer', () => {
 		it( 'should merge unwrapped nodes #5', () => {
 			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>';
-			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 );
 		} );
@@ -219,76 +219,82 @@ describe( 'writer', () => {
 		it( 'should unwrap mixed ranges #1', () => {
 			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>';
-			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 );
 		} );
 
 		it( 'should unwrap mixed ranges #2', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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>'
 			);
 		} );

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

@@ -42,16 +42,16 @@ describe( 'writer', () => {
 		it( 'wraps single text node', () => {
 			test(
 				'<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', () => {
 			test(
 				'{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', () => {
 			test(
 				'<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', () => {
 			test(
 				'<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', () => {
 			test(
 				'<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', () => {
 			test(
 				'<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', () => {
 			test(
 				'<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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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>' +
 				'[' +
-					'<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>'
 			);
@@ -189,45 +195,47 @@ describe( 'writer', () => {
 
 		it( 'should wrap single element by merging attributes', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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', () => {
 			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>' +
 				'[' +
-					'<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>'
 			);
@@ -235,27 +243,29 @@ describe( 'writer', () => {
 
 		it( 'should not merge single elements when they have different priority', () => {
 			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>' +
 				'[' +
-					'<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>'
 			);
 		} );
 
 		it( 'should be merged with outside element when wrapping all children', () => {
 			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>' +
 				'[' +
-					'<attribute:b:1 baz="qux" foo="bar">' +
+					'<attribute:b view-priority="1" baz="qux" foo="bar">' +
 						'foobar' +
-						'<attribute:i:1>baz</attribute:i:1>' +
-					'</attribute:b:1>' +
+						'<attribute:i view-priority="1">baz</attribute:i>' +
+					'</attribute:b>' +
 				']' +
 				'</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', () => {
 		test(
 			'<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', () => {
 		test(
 			'<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', () => {
 		test(
 			'<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', () => {
 		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', () => {
 		test(
 			'<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', () => {
 		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', () => {
 		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', () => {
 		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>' +
-				'<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>'
 		);
 	} );
 
 	it( 'should merge when wrapping between same attribute', () => {
 		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', () => {
 		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>'
 		);
 	} );
 } );