Przeglądaj źródła

Working on core.treeView.Writer wrap and unwrap methods.

Working on core.treeView.Writer wrap and unwrap methods.
Szymon Kupś 9 lat temu
rodzic
commit
bc74d0c0fb

+ 25 - 0
packages/ckeditor5-engine/src/treeview/range.js

@@ -39,6 +39,15 @@ export default class Range {
 		this.end = Position.createFromPosition( end );
 	}
 
+	/**
+	 * Returns whether the range is collapsed, that is it start and end positions are equal.
+	 *
+	 * @type {Boolean}
+	 */
+	get isCollapsed() {
+		return this.start.isEqual( this.end );
+	}
+
 	/**
 	 * Two ranges equal if their start and end positions equal.
 	 *
@@ -48,4 +57,20 @@ export default class Range {
 	isEqual( otherRange ) {
 		return this == otherRange || ( this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end ) );
 	}
+
+	/**
+	 * Creates a range from given parents and offsets.
+	 *
+	 * @param {core.treeView.Element} startElement Start position parent element.
+	 * @param {Number} startOffset Start position offset.
+	 * @param {core.treeView.Element} endElement End position parent element.
+	 * @param {Number} endOffset End position offset.
+	 * @returns {core.treeView.Range} Created range.
+	 */
+	static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
+		return new this(
+			new Position( startElement, startOffset ),
+			new Position( endElement, endOffset )
+		);
+	}
 }

+ 244 - 21
packages/ckeditor5-engine/src/treeview/writer.js

@@ -8,7 +8,9 @@
 import Position from './position.js';
 import Element from './element.js';
 import Text from './text.js';
+import Range from './range.js';
 import utils from '../utils.js';
+import CKEditorError from '../ckeditorerror.js';
 
 /**
  * Tree model Writer class.
@@ -71,18 +73,24 @@ import utils from '../utils.js';
 		return this._priorities.get( node );
 	}
 
-	insertIntoContainer( position, nodes ) {
-		const container = this.getParentContainer( position );
-
-		const insertionPosition = this.breakAttributes( position, container );
-
-		container.insertChildren( insertionPosition, nodes );
+	/**
+	 * Returns first parent container of specified position. Position's parent is checked as first, then
+	 * next parent is checked. Returns null if no parent container can be found.
+	 *
+	 * @param {core.treeView.Position} position
+	 * @returns {core.treeView.Element|null}
+	 */
+	getParentContainer( position ) {
+		let parent = position.parent;
 
-		const length = utils.isIterable( nodes ) ? utils.count( nodes ) : 1;
-		const endPosition = insertionPosition.getShiftedBy( length );
+		while ( !this.isContainer( parent ) ) {
+			if ( !parent ) {
+				return null;
+			}
+			parent = parent.parent;
+		}
 
-		this.mergeAttributes( endPosition );
-		this.mergeAttributes( insertionPosition );
+		return parent;
 	}
 
 	/**
@@ -147,20 +155,20 @@ import utils from '../utils.js';
 				return this.breakAttributes( newPosition );
 			} else {
 				// Break element.
-				const nodeClone = positionParent.clone();
+				const clonedNode = positionParent.clone();
 
 				// Clone priority.
-				this.setPriority( nodeClone, this.getPriority( positionParent ) );
+				this.setPriority( clonedNode, this.getPriority( positionParent ) );
 
 				// Insert cloned node to position's parent node.
-				positionParent.parent.insertChildren( offsetAfter, nodeClone );
+				positionParent.parent.insertChildren( offsetAfter, clonedNode );
 
 				// Get nodes to move.
 				const count = positionParent.getChildCount() - positionOffset;
 				const nodesToMove = positionParent.removeChildren( positionOffset, count );
 
 				// Move nodes to cloned node.
-				nodeClone.appendChildren( nodesToMove );
+				clonedNode.appendChildren( nodesToMove );
 
 				// Create new position to work on.
 				const newPosition = new Position( positionParent.parent, offsetAfter );
@@ -236,6 +244,26 @@ import utils from '../utils.js';
 		return position;
 	}
 
+	/**
+	 * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
+	 * and merging them after.
+	 *
+	 * @param {core.treeView.Position} position Insertion position.
+	 * @param {core.treeView.Node|Iterable.<core.treeView.Node>} nodes Node or nodes to insert.
+	 */
+	insert( position, nodes ) {
+		const container = this.getParentContainer( position );
+		const insertionPosition = this.breakAttributes( position );
+
+		container.insertChildren( insertionPosition.offset, nodes );
+
+		const length = utils.isIterable( nodes ) ? utils.count( nodes ) : 1;
+		const endPosition = insertionPosition.getShiftedBy( length );
+
+		this.mergeAttributes( endPosition );
+		this.mergeAttributes( insertionPosition );
+	}
+
 	//removeFromContainer( range ) {
 	//}
     //
@@ -245,15 +273,210 @@ import utils from '../utils.js';
 	//// <p>|</p>
 	//removeEmptyAttributes( position ) {
 	//}
-    //
+
 	//// f[o]o -> f<b>o</b>o
 	//// <b>f</b>[o]<b>o</b> -> <b>f</b><b>o</b><b>o</b> -> <b>foo</b>
 	//// <b>f</b>o[o<u>bo]m</u> -> <b>f</b>o<b>o</b><u><b>bo</b>m</u>
 	//// Range have to] be inside single container.
-	//wrap( range, element, priority ) {
-	//	// this._priorities.set( element, priority );
-	//}
-    //
-	//unwrap( range, element ) {
-	//}
+
+	/**
+	 * Wraps range with provided attribute element. Range's start and end positions should be placed inside same
+	 * container element. Method will wrap elements inside that container but wil not enter nested containers.
+	 *
+	 * Throws {@link core.CKEditorError} `treeview-writer-wrap-invalid-range` when {@link core.treeView.Range#end} and
+	 * {@link core.treeView.Range#start} position are not placed inside same parent container node.
+	 *
+	 * @param range
+	 * @param element
+	 * @param priority
+	 */
+	wrap( range, element, priority ) {
+		const rangeStart = range.start;
+		const rangeEnd = range.end;
+
+		// Range should be placed inside one container.
+		if ( this.getParentContainer( rangeStart ) !== this.getParentContainer( rangeEnd ) ) {
+			/**
+			 * Range is not placed inside same container.
+			 *
+			 * @error treeview-writer-wrap-invalid-range
+			 */
+			throw new CKEditorError( 'treeview-writer-wrap-invalid-range' );
+		}
+
+		// If range is collapsed - nothing to wrap.
+		if ( range.isCollapsed ) {
+			return range;
+		}
+
+		// Sets wrapper element priority.
+		this.setPriority( element, priority );
+
+		// Break attributes at range start and end.
+		const breakStart = this.breakAttributes( rangeStart );
+		const breakEnd = this.breakAttributes( rangeEnd );
+		const parentBlock = breakStart.parent;
+
+		// Wrap range. Start collapsed.
+		let wrapRange = new Range( breakStart, breakStart );
+
+		// Iterate over all elements inside break points.
+		for ( let i = breakStart.offset; i < breakEnd.offset; i++ ) {
+			const child = parentBlock.getChild( i );
+
+			if ( child instanceof Text ) {
+				wrapRange.end.offset++;
+			} else {
+				// Don't enter container nodes. Wrap them.
+				if ( this.isContainer( child ) ) {
+					// mark as true to wrap
+					wrapRange.end.offset++;
+				} else {
+					// if child is same as element - unwrap its contents
+					if ( child.same( element ) && priority == this.getPriority( child ) ) {
+						// unwrap
+					} else {
+						// scan elements children for the same atribute and check if cannot be changes to be at the top
+						// if yes - unwrap it
+						// if no mark this child as unable to wrap, go deeper and try to wrap.
+					}
+				}
+			}
+		}
+
+		// Get nodes to move.
+		const count = wrapRange.end.offset - wrapRange.start.offset;
+		const nodesToMove = parentBlock.removeChildren( wrapRange.start.offset, count );
+		//
+		//// TODO: What if element already have some children?
+		//element.insertChildren( 0, nodesToMove );
+		//
+		//// Insert wrapper with elements to parent container.
+		//parentBlock.insertChildren( breakStart.offset, element );
+		//
+		//// Return new range around wrapper.
+		//return new Range( breakStart, new Position( breakStart.parent , breakStart.offset + 1 ) );
+	}
+
+	/**
+	 * Unwraps elements within provided range from element attribute. Range's start and end positions should be placed
+	 * inside same  container element.
+	 *
+	 * Throws {@link core.CKEditorError} `treeview-writer-wrap-invalid-range` when {@link core.treeView.Range#end} and
+	 * {@link core.treeView.Range#start} position are not placed inside same parent container node.
+	 *
+	 * @param range
+	 * @param element
+	 */
+	unwrap( range, attribute ) {
+		const rangeStart = range.start;
+		const rangeEnd = range.end;
+
+		// Range should be placed inside one container.
+		if ( this.getParentContainer( rangeStart ) !== this.getParentContainer( rangeEnd ) ) {
+			/**
+			 * Range is not placed inside same container.
+			 *
+			 * @error treeview-writer-wrap-invalid-range
+			 */
+			throw new CKEditorError( 'treeview-writer-unwrap-invalid-range' );
+		}
+
+		// If range is collapsed - nothing to unwrap.
+		if ( range.isCollapsed ) {
+			return range;
+		}
+
+		// Break attributes at range start and end.
+		const breakStart = this.breakAttributes( rangeStart );
+		const breakEnd = this.breakAttributes( rangeEnd );
+		const parentContainer = breakStart.parent;
+
+		// Unwrap children located between break points.
+		const newRange = this.unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
+
+		// Merge attributes at the both ends and return a new range.
+		const end = this.mergeAttributes( newRange.end );
+		const start = this.mergeAttributes( newRange.start );
+
+		return new Range( start, end );
+	}
+
+	unwrapChildren( parent, startOffset, endOffset, attribute ) {
+		let i = startOffset;
+		const unwrapPositions = [];
+
+		// Iterate over each element between provided offsets inside parent.
+		while ( i < endOffset ) {
+			const child = parent.getChild( i );
+
+			// If attributes are the same and have same priority, then unwrap.
+			if (  child.same( attribute ) && this.getPriority( child ) == this.getPriority( attribute ) ) {
+				const unwrapped = child.getChildren();
+				const count = child.getChildCount();
+
+				// Replace wrapper element with its children
+				child.remove();
+				parent.insertChildren( i, unwrapped );
+
+				// Save start and end position of moved items.
+				unwrapPositions.push(
+					new Position( parent, i ),
+					new Position( parent, i + count )
+				);
+
+				// Skip elements that were unwrapped. Assuming that there won't be another element to unwrap in child
+				// elements.
+				i += count;
+				endOffset += count - 1;
+			} else {
+				// If nested attribute is found - start unwrapping there.
+				if ( this.isAttribute( child ) ) {
+					this.unwrapChildren( child, 0, child.getChildCount(), attribute );
+				}
+
+				i++;
+			}
+		}
+
+		// Merge at each unwrap.
+		let offsetChange = 0;
+
+		for ( let position of unwrapPositions ) {
+			position.offset -= offsetChange;
+
+			// Do not merge with elements outside selected children.
+			if ( position.offset == startOffset || position.offset == endOffset ) {
+				continue;
+			}
+
+			const newPosition = this.mergeAttributes( position );
+
+			// If nodes were merged - other merge offsets will change.
+			if ( !newPosition.isEqual( position ) ) {
+				offsetChange++;
+				endOffset--;
+			}
+		}
+
+		return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
+	}
+
 }
+
+// 1. If wrapper element is container - wrap everything with that container.
+// 2. If wrapping with attribute element:
+//		2.1. Take a child list.
+//		2.2. Check if each node can be wrapped by provided element.
+//		2.3a. If each node can be wrapped - wrap all nodes.
+//		2.3b. If some nodes cannot be wrapped - wrap groups that can be wrapped.
+//		2.4. For each node that could not be wrapped - go to step 2.1.
+
+// <p>[foo<u>bar<c>baz<b>quz</b>toz</c>fli</u>bak]</p>
+
+// 1. Wrapping with container.
+// <div>[<p class="test">foobar</p><p>bazqux</p>]</div>
+// 2. Wrapping containers:
+// <article>[<p>foobar</p><p>bazqux</p>]</article>
+// 3. Nested elements.
+// <p>[foo<span>this is <u><b>bolded</b></u> text</span>foobar]</p>

+ 1038 - 259
packages/ckeditor5-engine/tests/treeview/writer.js

@@ -11,8 +11,151 @@ import Writer from '/ckeditor5/core/treeview/writer.js';
 import Element from '/ckeditor5/core/treeview/element.js';
 import Text from '/ckeditor5/core/treeview/text.js';
 import Position from '/ckeditor5/core/treeview/position.js';
+import Range from '/ckeditor5/core/treeview/range.js';
 
 describe( 'Writer', () => {
+	/**
+	 * Helper function that is used to test output of writer methods by providing declarative description of the
+	 * expected output.
+	 * Examples:
+	 * 		test element: `<p>fo[o<b>ba]r</b></p>`
+	 * 		description: {
+	 * 			name: 'p',
+	 * 	    	instanceOf: Element,
+	 * 	    	children:[
+	 * 	    		{
+	 * 	    			instanceOf: Text,
+	 * 	    			data: 'foo',
+	 * 	    			rangeStart: 2
+ 	 * 	    		},
+	 *				{
+	 *					name: 'b'
+	 *					instanceOf: Element
+	 *					priority: 1,
+	 *					children: [
+	 *						{ instanceOf: Text, data: 'bar', rangeEnd: 2 }
+	 *					]
+	 *				}
+	 * 	    	]
+	 * 		}
+	 *
+	 *
+	 * @param {core.treeView.Writer} writer Writer instance. Used to test priority.
+	 * @param {core.treeView.Range|core.treeView.Position } location Range instance or Position instance.
+	 * Treated as Range when when `rangeStart`, `rangeEnd` is used, treated as Position when `position` is used.
+	 * @param {core.treeView.Node} node Element to check.
+	 * @param {Object} description Object describing expected element and its children.
+	 */
+	function test( writer, location, node, description ) {
+		if ( description.instanceOf ) {
+			expect( node ).to.be.instanceof( description.instanceOf );
+		}
+
+		if ( description.name ) {
+			expect( node.name ).to.equal( description.name );
+		}
+
+		if ( description.data ) {
+			expect( node.data ).to.equal( description.data );
+		}
+
+		if ( description.priority !== undefined ) {
+			expect( writer.getPriority( node ) ).to.equal( description.priority );
+		}
+
+		if ( description.rangeStart !== undefined ) {
+			expect( location.start.parent ).to.equal( node );
+			expect( location.start.offset ).to.equal( description.rangeStart );
+		}
+
+		if ( description.attributes ) {
+			Object.keys( description.attributes ).forEach( ( key ) => {
+				expect( node.getAttribute( key ) ).to.equal( description.attributes[ key ] );
+			} );
+		}
+
+		if ( description.rengeEnd !== undefined ) {
+			expect( location.end.parent ).to.equal( node );
+			expect( location.end.offset ).to.equal( description.rengeEnd );
+		}
+
+		if ( description.position !== undefined ) {
+			expect( location.parent ).to.equal( node );
+			expect( location.offset ).to.equal( description.position );
+		}
+
+		if ( description.children ) {
+			expect( node.getChildCount() ).to.equal( description.children.length );
+			description.children.forEach( ( desc, index ) => {
+				test( writer, location, node.getChild( index ), desc );
+			} );
+		}
+	}
+
+	/**
+	 * Helper function that is used to create treeView elements from description object.
+	 *
+	 * @param {core.treeView.Writer} writer Writer instance. Used to set priorities.
+	 * @param {Object} description Description object.
+	 * @param {core.treeView.Range} [range] Optional parameter, used in recurrent calls.
+	 * @param {core.treeView.Position} [position] Optional parameter, used in recurrent calls.
+	 * @returns {Object} Returns object with `node`, `range`, `position` fields, containing created node and, optionally
+	 * range and position if description object contain information about them.
+	 */
+	function create( writer, description, range, position ) {
+		const node = new description.instanceOf();
+
+		if ( !range ) {
+			range = Range.createFromParentsAndOffsets( node, 0, node, 0 );
+		}
+
+		if ( !position ) {
+			position = new Position( node, 0 );
+		}
+
+		if ( description.name ) {
+			node.name = description.name;
+		}
+
+		if ( description.data ) {
+			node.data = description.data;
+		}
+
+		if ( description.attributes ) {
+			Object.keys( description.attributes ).forEach( ( key ) => {
+				node.setAttribute( key, description.attributes[ key ] );
+			} );
+		}
+
+		if ( description.priority !== undefined ) {
+			writer.setPriority( node, description.priority );
+		}
+
+		if ( description.rangeStart !== undefined ) {
+			range.start.parent = node;
+			range.start.offset = description.rangeStart;
+		}
+
+		if ( description.rangeEnd !== undefined ) {
+			range.end.parent = node;
+			range.end.offset = description.rangeEnd;
+		}
+
+		if ( description.position !== undefined ) {
+			position.parent = node;
+			position.offset = description.position;
+		}
+
+		if ( description.children ) {
+			description.children.forEach( ( desc, index ) => {
+				const created = create( writer, desc, range, position );
+				node.insertChildren( index, created.node );
+			} );
+		}
+
+		return { node, range, position };
+	}
+
 	describe( 'isContainer', () => {
 		it( 'should return true for container elements', () => {
 			const containerElement = new Element( 'p' );
@@ -60,337 +203,973 @@ describe( 'Writer', () => {
 	} );
 
 	describe( 'breakAttributes', () => {
-		// Start position is at the begin of the text node.
-		// After breaking it should be at the begin of the 'p' element.
-		it( '<p>|foobar</p>', () => {
+		// <p>{|foobar}</p> -> <p>|{foobar}</p>
+		it( '<p>{|foobar}</p>', () => {
 			const writer = new Writer();
-			const text = new Text( 'foobar' );
-			const element = new Element( 'p', {}, [ text ] );
-			const position = new Position( text, 0 );
-
-			const newPosition = writer.breakAttributes( position );
-			const newParent = newPosition.parent;
-
-			expect( newPosition.offset ).to.equal( 0 );
-			expect( newParent ).to.equal( element );
-			expect( newParent.getChildCount() ).to.equal( 1 );
-			expect( newParent.getChild( 0 ) ).to.equal( text );
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{ instanceOf: Text, data: 'foobar', position: 0 }
+				]
+			} );
+
+			const newPosition = writer.breakAttributes( created.position );
+
+			test( writer, newPosition, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				position: 0,
+				children: [
+					{ instanceOf: Text, data: 'foobar' }
+				]
+			} );
 		} );
 
-		// Start position is located in the middle of text node.
-		// After breaking it should be located inside 'p' element, between two text nodes 'foo' and 'bar'.
 		it( '<p>foo|bar</p>', () => {
+			// <p>{foo|bar}</p> -> <p>{foo}|{bar}</p>
 			const writer = new Writer();
-			const text = new Text( 'foobar' );
-			const element = new Element( 'p', {}, [ text ] );
-			const position = new Position( text, 3 );
-
-			const newPosition = writer.breakAttributes( position );
-			const newParent = newPosition.parent;
-
-			expect( newPosition.offset ).to.equal( 1 );
-			expect( newParent ).to.equal( element );
-			expect( newParent.getChildCount() ).to.equal( 2 );
-			expect( newParent.getChild( 0 ) ).to.be.instanceOf( Text );
-			expect( newParent.getChild( 1 ) ).to.be.instanceOf( Text );
-			expect( newParent.getChild( 0 ).data ).to.equal( 'foo' );
-			expect( newParent.getChild( 1 ).data ).to.equal( 'bar' );
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{ instanceOf: Text, data: 'foobar', position: 3 }
+				]
+			} );
+
+			const newPosition = writer.breakAttributes( created.position );
+
+			test( writer, newPosition, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				position: 1,
+				children: [
+					{ instanceOf: Text, data: 'foo' },
+					{ instanceOf: Text, data: 'bar' }
+				]
+			} );
 		} );
 
-		// Start position is at the end of the text node.
-		// After breaking it should be at the end of the 'p' element.
-		it( '<p>foobar|</p>', () => {
+		it( '<p>{foobar|}</p>', () => {
+			// <p>{foobar|}</p> -> <p>{foobar}|</p>
 			const writer = new Writer();
-			const text = new Text( 'foobar' );
-			const element = new Element( 'p', {}, [ text ] );
-			const position = new Position( text, 6 );
-
-			const newPosition = writer.breakAttributes( position );
-			const newParent = newPosition.parent;
-
-			expect( newPosition.offset ).to.equal( 1 );
-			expect( newParent ).to.equal( element );
-			expect( newParent.getChildCount() ).to.equal( 1 );
-			expect( newParent.getChild( 0 ) ).to.equal( text );
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{ instanceOf: Text, data: 'foobar', position: 6 }
+				]
+			} );
+
+			const newPosition = writer.breakAttributes( created.position );
+
+			test( writer, newPosition, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				position: 1,
+				children: [
+					{ instanceOf: Text, data: 'foobar' }
+				]
+			} );
 		} );
 
-		// <p><b>foo|bar</b></p> -> <p><b>foo</b>|<b>bar</b></p>
-		it( '<p><b>foo|bar</b></p>', () => {
+		it( '<p><b>{foo|bar}</b></p>', () => {
+			// <p><b>{foo|bar}</b></p> -> <p><b>{foo}</b>|<b>{bar}</b></p>
 			const writer = new Writer();
-			const text = new Text( 'foobar' );
-			const b = new Element( 'b', { attribute: 'value' }, [ text ] );
-			const p = new Element( 'p', {}, [ b ] );
-			const position = new Position( text, 3 );
-			writer.setPriority( b, 1 );
 
-			const newPosition = writer.breakAttributes( position );
-			const parent = newPosition.parent;
-
-			expect( parent ).to.equal( p );
-			expect( newPosition.offset ).to.equal( 1 );
-			expect( p.getChildCount() ).to.equal( 2 );
-			const child1 = p.getChild( 0 );
-			const child2 = p.getChild( 1 );
-			expect( child1 ).to.be.instanceOf( Element );
-			expect( child1.same( b ) ).to.be.true;
-			expect( writer.getPriority( child1 ) ).to.equal( 1 );
-			expect( child1.getChildCount() ).to.equal( 1 );
-			expect( child1.getChild( 0 ).data ).to.equal( 'foo' );
-			expect( child2 ).to.be.instanceOf( Element );
-			expect( child2.same( b ) ).to.be.true;
-			expect( writer.getPriority( child2 ) ).to.equal( 1 );
-			expect( child2.getChildCount() ).to.equal( 1 );
-			expect( child2.getChild( 0 ).data ).to.equal( 'bar' );
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{ instanceOf: Text, data: 'foobar', position: 3 }
+						]
+					}
+				]
+			} );
+
+			const newPosition = writer.breakAttributes( created.position );
+
+			test( writer, newPosition, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				position: 1,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{ instanceOf: Text, data: 'foo' }
+						]
+					},
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{ instanceOf: Text, data: 'bar' }
+						]
+					}
+				]
+			} );
 		} );
 
-		// <p><b><u>|foobar</u></b></p> -> <p>|<b><u>foobar</u></b></p>
-		it( '<p><b><u>|foobar</u></b></p>', () => {
+		it( '<p><b><u>{|foobar}</u></b></p>', () => {
+			// <p><b><u>{|foobar}</u></b></p> -> <p>|<b><u>{foobar}</u></b></p>
 			const writer = new Writer();
-			const text = new Text( 'foobar' );
-			const u = new Element( 'u', { foo: 'bar' }, [ text ] );
-			const b = new Element( 'b', { baz: 'qux' }, [ u ] );
-			const p = new Element( 'p', {}, [ b ] );
-			const position = new Position( text, 0 );
-			writer.setPriority( u, 10 );
-			writer.setPriority( b, 5 );
-
-			const newPosition = writer.breakAttributes( position );
-			const parent = newPosition.parent;
-
-			expect( parent ).to.equal( p );
-			expect( newPosition.offset ).to.equal( 0 );
-			expect( p.getChildCount() ).to.equal( 1 );
-			const b1 = p.getChild( 0 );
-			expect( b1 ).to.equal( b );
-			expect( writer.getPriority( b1 ) ).to.equal( 5 );
-			expect( b1.getChildCount() ).to.equal( 1 );
-			const u1 = b.getChild( 0 );
-			expect( u1 ).to.equal( u );
-			expect( writer.getPriority( u1 ) ).to.equal( 10 );
-			expect( u1.getChildCount() ).to.equal( 1 );
-			const text1 = u1.getChild( 0 );
-			expect( text1 ).to.equal( text );
-			expect( text1.data ).to.equal( 'foobar' );
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{
+								instanceOf: Element,
+								name: 'u',
+								priority: 1,
+								children: [
+									{ instanceOf: Text, data: 'foobar', position: 0 }
+								]
+							}
+						]
+					}
+				]
+			} );
+
+			const newPosition = writer.breakAttributes( created.position );
+
+			test( writer, newPosition, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				position: 0,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{
+								instanceOf: Element,
+								name: 'u',
+								priority: 1,
+								children: [
+									{ instanceOf: Text, data: 'foobar' }
+								]
+							}
+						]
+					}
+				]
+			} );
 		} );
 
-		// <p><b><u>foo|bar</u></b></p> -> <p><b><u>foo</u></b>|<b></u>bar</u></b></p>
-		it( '<p><b><u>foo|bar</u></b></p>', () => {
+		// <p><b><u>{foo|ba}r</u></b></p> -> <p><b><u>{foo}</u></b>|<b></u>{bar}</u></b></p>
+		it( '<p><b><u>{foo|bar}</u></b></p>', () => {
 			const writer = new Writer();
-			const text = new Text( 'foobar' );
-			const u = new Element( 'u', { foo: 'bar' }, [ text ] );
-			const b = new Element( 'b', { baz: 'qux' }, [ u ] );
-			const p = new Element( 'p', {}, [ b ] );
-			const position = new Position( text, 3 );
-			writer.setPriority( u, 1 );
-			writer.setPriority( b, 1 );
-
-			const newPosition = writer.breakAttributes( position );
-			const parent = newPosition.parent;
-
-			expect( parent ).to.equal( p );
-			expect( newPosition.offset ).to.equal( 1 );
-			expect( parent.getChildCount() ).to.equal( 2 );
-			const b1 = parent.getChild( 0 );
-			const b2 = parent.getChild( 1 );
-			expect( b1.same( b ) ).to.be.true;
-			expect( b2.same( b ) ).to.be.true;
-			expect( b1.getChildCount() ).to.equal( 1 );
-			expect( b2.getChildCount() ).to.equal( 1 );
-
-			const u1 = b1.getChild( 0 );
-			const u2 = b2.getChild( 0 );
-
-			expect( u1.same( u ) ).to.be.true;
-			expect( u2.same( u ) ).to.be.true;
-
-			expect( u1.getChildCount() ).to.equal( 1 );
-			expect( u2.getChildCount() ).to.equal( 1 );
-			expect( u1.getChild( 0 ).data ).to.equal( 'foo' );
-			expect( u2.getChild( 0 ).data ).to.equal( 'bar' );
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{
+								instanceOf: Element,
+								name: 'u',
+								priority: 1,
+								children: [
+									{ instanceOf: Text, data: 'foobar', position: 3 }
+								]
+							}
+						]
+					}
+				]
+			} );
+
+			const newPosition = writer.breakAttributes( created.position );
+
+			test( writer, newPosition, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				position: 1,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{
+								instanceOf: Element,
+								name: 'u',
+								priority: 1,
+								children: [
+									{ instanceOf: Text, data: 'foo' }
+								]
+							}
+						]
+					},
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{
+								instanceOf: Element,
+								name: 'u',
+								priority: 1,
+								children: [
+									{ instanceOf: Text, data: 'bar' }
+								]
+							}
+						]
+					}
+				]
+			} );
 		} );
 
-		// <p><b><u>foobar|</u></b></p> -> <p><b><u>foobar</u></b>|</p>
-		it( '<p><b><u>foobar|</u></b></p>', () => {
+		it( '<p><b><u>{foobar|}</u></b></p>', () => {
+			// <p><b><u>{foobar|}</u></b></p> -> <p><b><u>{foobar}</u></b>|</p>
 			const writer = new Writer();
-			const text = new Text( 'foobar' );
-			const u = new Element( 'u', { foo: 'bar' }, [ text ] );
-			const b = new Element( 'b', { baz: 'qux' }, [ u ] );
-			const p = new Element( 'p', {}, [ b ] );
-			const position = new Position( text, 6 );
-			writer.setPriority( u, 1 );
-			writer.setPriority( b, 1 );
-
-			const newPosition = writer.breakAttributes( position );
-			const parent = newPosition.parent;
-
-			expect( parent ).to.equal( p );
-			expect( newPosition.offset ).to.equal( 1 );
-			expect( p.getChildCount() ).to.equal( 1 );
-			const b1 = p.getChild( 0 );
-			expect( b1.same( b ) ).to.be.true;
-			expect( b1.getChildCount() ).to.equal( 1 );
-			const u1 = b.getChild( 0 );
-			expect( u1.same( u ) ).to.be.true;
-			expect( u1.getChildCount() ).to.equal( 1 );
-			const text1 = u1.getChild( 0 );
-			expect( text1.same( text ) ).to.be.true;
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{
+								instanceOf: Element,
+								name: 'u',
+								priority: 1,
+								children: [
+									{ instanceOf: Text, data: 'foobar', position: 6 }
+								]
+							}
+						]
+					}
+				]
+			} );
+
+			const newPosition = writer.breakAttributes( created.position );
+
+			test( writer, newPosition, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				position: 1,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{
+								instanceOf: Element,
+								name: 'u',
+								priority: 1,
+								children: [
+									{ instanceOf: Text, data: 'foobar' }
+								]
+							}
+						]
+					}
+				]
+			} );
 		} );
 	} );
 
 	describe( 'mergeAttributes', () => {
-		// <p>{fo|obar}</p>
 		it( 'should not merge if inside text node', () => {
+			// <p>{fo|obar}</p>
 			const writer = new Writer();
-			const text = new Text( 'foobar' );
-			const p = new Element( 'p', [], [ text ] );
-			const position = new Position( text, 2 );
-
-			const newPosition = writer.mergeAttributes( position );
-
-			expect( newPosition.isEqual( position ) ).to.be.true;
-			expect( p.getChildCount() ).to.equal( 1 );
-			const text1 = p.getChild( 0 );
-			expect( text1 ).to.equal( text );
-			expect( text1.data ).to.equal( 'foobar' );
+			const description = {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{ instanceOf: Text, data: 'foobar', position: 2 }
+				]
+			};
+			const created = create( writer, description );
+			const newPosition = writer.mergeAttributes( created.position );
+			test( writer, newPosition, created.node, description );
 		} );
 
 		it( 'should return same position when inside empty container', () => {
 			// <p>|</p>
 			const writer = new Writer();
-			const p = new Element( 'p' );
-			const position = new Position( p, 0 );
-
-			const newPosition = writer.mergeAttributes( position );
-
-			expect( newPosition.isEqual( position ) ).to.be.true;
+			const description = { instanceOf: Element, name: 'p', position: 0 };
+			const created = create( writer, description );
+			const newPosition = writer.mergeAttributes( created.position );
+			test( writer, newPosition, created.node, description );
 		} );
 
 		it( 'should not merge when position is placed at the beginning of the container', () => {
 			// <p>|<b></b></p>
 			const writer = new Writer();
-			const b = new Element( 'b' );
-			const p = new Element( 'p', {}, [ b ] );
-			const position = new Position( p, 0 );
-			writer.setPriority( b, 1 );
-
-			const newPosition = writer.mergeAttributes( position );
-
-			expect( newPosition.isEqual( position ) );
-			expect( p.getChildCount() ).to.equal( 1 );
-			expect( p.getChild( 0 ) ).to.equal( b );
+			const description = {
+				instanceOf: Element,
+				name: 'p',
+				position: 0,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1
+					}
+				]
+			};
+			const created = create( writer, description );
+			const newPosition = writer.mergeAttributes( created.position );
+			test( writer, newPosition, created.node, description );
 		} );
 
 		it( 'should not merge when position is placed at the end of the container', () => {
 			// <p><b></b>|</p>
 			const writer = new Writer();
-			const b = new Element( 'b' );
-			const p = new Element( 'p', {}, [ b ] );
-			const position = new Position( p, 1 );
-			writer.setPriority( b, 1 );
+			const description = {
+				instanceOf: Element,
+				name: 'p',
+				position: 1,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1
+					}
+				]
+			};
+			const created = create( writer, description );
+			const newPosition = writer.mergeAttributes( created.position );
+			test( writer, newPosition, created.node, description );
+		} );
 
-			const newPosition = writer.mergeAttributes( position );
+		it( 'should merge when placed between two text nodes', () => {
+			// <p>{foo}|{bar}</p> -> <p>{foo|bar}</p>
+			const writer = new Writer();
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				position: 1,
+				children: [
+					{ instanceOf: Text, data: 'foo' },
+					{ instanceOf: Text, data: 'bar' }
+				]
+			} );
+
+			const newPosition = writer.mergeAttributes( created.position );
+
+			test( writer, newPosition, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{ instanceOf: Text, data: 'foobar', position: 3 }
+				]
+			} );
+		} );
 
-			expect( newPosition.isEqual( position ) );
-			expect( p.getChildCount() ).to.equal( 1 );
-			expect( p.getChild( 0 ) ).to.equal( b );
+		it( 'should merge when placed between similar attribute nodes', () => {
+			// <p><b foo="bar"></b>|<b foo="bar"></b></p> -> <p><b foo="bar">|</b></p>
+			const writer = new Writer();
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				position: 1,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						attributes: { foo: 'bar' }
+					},
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						attributes: { foo: 'bar' }
+					}
+				]
+			} );
+
+			const newPosition = writer.mergeAttributes( created.position );
+
+			test( writer, newPosition, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						position: 0,
+						attributes: { foo: 'bar' }
+					}
+				]
+			} );
 		} );
 
-		it( 'should merge when placed between two text nodes', () => {
-			// <p>{foo}|{bar}</p>
-			// <p>{foo|bar}</p>
+		it( 'should not merge when placed between non-similar attribute nodes', () => {
+			// <p><b foo="bar"></b>|<b foo="baz"></b></p> ->
+			// <p><b foo="bar"></b>|<b foo="baz"></b></p>
 			const writer = new Writer();
-			const text1 = new Text( 'foo' );
-			const text2 = new Text( 'bar' );
-			const p = new Element( 'p', {}, [ text1, text2 ] );
-			const position = new Position( p, 1 );
+			const description = {
+				instanceOf: Element,
+				name: 'p',
+				position: 1,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						attributes: { foo: 'bar' }
+					},
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						attributes: { foo: 'baz' }
+					}
+				]
+			};
+			const created = create( writer, description );
+			const newPosition = writer.mergeAttributes( created.position );
+			test( writer, newPosition, created.node, description );
+		} );
 
-			const newPosition = writer.mergeAttributes( position );
+		it( 'should not merge when placed between similar attribute nodes with different priority', () => {
+			// <p><b foo="bar"></b>|<b foo="bar"></b></p> -> <p><b foo="bar"></b>|<b foo="bar"></b></p>
+			const writer = new Writer();
+			const description =  {
+				instanceOf: Element,
+				name: 'p',
+				position: 1,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						attributes: { foo: 'bar' }
+					},
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 2,
+						attributes: { foo: 'bar' }
+					}
+				]
+			};
+			const created = create( writer,description );
+			const newPosition = writer.mergeAttributes( created.position );
+			test( writer, newPosition, created.node, description );
+		} );
 
-			expect( newPosition.parent ).is.equal( text1 );
-			expect( newPosition.offset ).is.equal( 3 );
-			expect( text1.data ).to.equal( 'foobar' );
-			expect( p.getChildCount() ).to.equal( 1 );
-			expect( p.getChild( 0 ) ).to.equal( text1 );
+		it( 'should merge attribute nodes and their contents if possible', () => {
+			// <p><b foo="bar">{foo}</b>|<b foo="bar">{bar}</b></p>
+			// <p><b foo="bar">{foo}|{bar}</b></p>
+			// <p><b foo="bar">{foo|bar}</b></p>
+			const writer = new Writer();
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				position: 1,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						attributes: { foo: 'bar' },
+						children: [
+							{ instanceOf: Text, data: 'foo' }
+						]
+					},
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						attributes: { foo: 'bar' },
+						children: [
+							{ instanceOf: Text, data: 'bar' }
+						]
+					}
+				]
+			} );
+
+			const newPosition = writer.mergeAttributes( created.position );
+
+			test( writer, newPosition, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						attributes: { foo: 'bar' },
+						children: [
+							{ instanceOf: Text, data: 'foobar', position: 3 }
+						]
+					}
+				]
+			} );
 		} );
+	} );
 
-		it( 'should merge when placed between similar attribute nodes', () => {
-			// <p><b foo="bar"></b>|<b foo="bar"></b></p>
-			// <p><b foo="bar">|</b></p>
+	describe( 'insert', () => {
+		it( 'should insert text', () => {
+			// <p>{foo|bar}</p> insert {baz}
+			// <p>{foo[baz]bar}</p>
+			const writer = new Writer();
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{ instanceOf: Text, data: 'foobar', position: 3 }
+				]
+			} );
+
+			const newRange = writer.insert( created.position, new Text( 'baz' ) );
+			test( writer, newRange, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				children: [
+					{ instanceOf: Text, data: 'foobazbar' }
+				]
+			} );
+		} );
+
+		it( 'should merge attributes', () => {
+			// <p><b>{foo|bar}</b></p> insert <b>{baz}</b>
+			// <p><b>{foobazbar}</b></p>
 			const writer = new Writer();
-			const b1 = new Element( 'b', { foo: 'bar' } );
-			const b2 = new Element( 'b', { foo: 'bar' } );
-			const p = new Element( 'p', {}, [ b1, b2 ] );
-			const position = new Position( p, 1 );
+			const text = new Text( 'foobar' );
+			const b1 = new Element( 'b', null, text );
+			const p = new Element( 'p', null, b1 );
+			const position = new Position( text, 3 );
+			const insertText = new Text( 'baz' );
+			const b2 = new Element( 'b', null, insertText );
+
 			writer.setPriority( b1, 1 );
 			writer.setPriority( b2, 1 );
 
-			const newPosition = writer.mergeAttributes( position );
+			writer.insert( position, b2 );
 
-			expect( newPosition.parent ).to.equal( b1 );
-			expect( newPosition.offset ).to.equal( 0 );
 			expect( p.getChildCount() ).to.equal( 1 );
-			expect( p.getChild( 0 ) ).to.equal( b1 );
+			const b3 = p.getChild( 0 );
+			expect( b3 ).to.be.instanceof( Element );
+			expect( b3.name ).to.equal( 'b' );
+			expect( b3.getChildCount() ).to.equal( 1 );
+			const newText = b3.getChild( 0 );
+			expect( newText ).to.be.instanceof( Text );
+			expect( newText.data ).to.equal( 'foobazbar' );
 		} );
+	} );
 
-		it( 'should not merge when placed between non-similar attribute nodes', () => {
-			// <p><b foo="bar"></b>|<b foo="baz"></b></p>
-			// <p><b foo="bar"></b>|<b foo="baz"></b></p>
+	describe( 'wrap', () => {
+		//// <p>[{foobar}]</p>
+		//// wrap <b>
+		//// <p>[<b>{foobar}<b>]</p>
+		//it( 'wraps single text node', () => {
+		//	const writer = new Writer();
+		//	const text = new Text( 'foobar' );
+		//	const p = new Element( 'p', null, [ text ] );
+		//	const b = new Element( 'b' );
+		//	const range = new Range(
+		//		new Position( p, 0 ),
+		//		new Position( p, 1 )
+		//	);
+		//
+		//	const newRange = writer.wrap( range, b, 1 );
+		//
+		//	expect( p.getChildCount() ).to.equal( 1 );
+		//	expect( p.getChild( 0 ) ).to.equal( b );
+		//	expect( b.getChildCount() ).to.equal( 1 );
+		//	expect( b.getChild( 0 ) ).to.equal( text );
+		//	expect( text.data ).to.equal( 'foobar' );
+		//	expect( newRange.start.parent ).to.equal( p );
+		//	expect( newRange.start.offset ).to.equal( 0 );
+		//	expect( newRange.end.parent ).to.equal( p );
+		//	expect( newRange.end.offset ).to.equal( 1 );
+		//} );
+
+		//// <p>[{foo]bar}</p>
+		//// <p>[<b>{foo}</b>]{bar}</p>
+		//it( 'wraps part of single text node', () => {
+		//	const writer = new Writer();
+		//	const text = new Text( 'foobar' );
+		//	const p = new Element( 'p', null, [ text ] );
+		//	const b = new Element( 'b' );
+		//	const range = new Range(
+		//		new Position( p, 0 ),
+		//		new Position( text, 3 )
+		//	);
+		//
+		//	const newRange = writer.wrap( range, b, 1 );
+		//	expect( p.getChildCount() ).to.equal( 2 );
+		//	const child1 = p.getChild( 0 );
+		//	const child2 = p.getChild( 1 );
+		//
+		//	expect( child1 ).to.be.instanceof( Element );
+		//	expect( child1.same( b ) ).to.be.true;
+		//	expect( child1.getChildCount() ).to.equal( 1 );
+		//	const newText = child1.getChild( 0 );
+		//	expect( newText ).to.be.instanceof( Text );
+		//	expect( newText.data ).to.equal( 'foo' );
+		//	expect( child2 ).to.be.instanceof( Text );
+		//	expect( child2.data ).to.equal( 'bar' );
+		//
+		//	expect( newRange.start.parent ).to.equal( p );
+		//	expect( newRange.start.offset ).to.equal( 0 );
+		//	expect( newRange.end.parent ).to.equal( p );
+		//	expect( newRange.end.offset ).to.equal( 1 );
+		//} );
+
+		//it( 'tests', () => {
+		//	const writer = new Writer();
+		//	const text = new Text( 'foobar' );
+		//	const text2 = new Text( 'bazquix' );
+		//	const u = new Element( 'u', null, [ text2 ] );
+		//	const p = new Element( 'p', null, [ text, u ] );
+		//	const b = new Element( 'b' );
+		//	const range = new Range(
+		//		new Position( p, 0 ),
+		//		new Position( p, 2 )
+		//	);
+		//
+		//	const newRange = writer.wrap( range, b, 1 );
+		//
+		//	expect( p.getChildCount() ).to.equal( 1 );
+		//	//expect( p.getChild( 0 ) ).to.equal( b );
+		//	//expect( b.getChildCount() ).to.equal( 1 );
+		//	//expect( b.getChild( 0 ) ).to.equal( text );
+		//	//expect( text.data ).to.equal( 'foobar' );
+		//	//expect( newRange.start.parent ).to.equal( p );
+		//	//expect( newRange.start.offset ).to.equal( 0 );
+		//	//expect( newRange.end.parent ).to.equal( p );
+		//	//expect( newRange.end.offset ).to.equal( 1 );
+		//} );
+	} );
+
+	describe( 'unwrap', () => {
+		it( 'should do nothing on single text node', () => {
+			// <p>[{foobar}]</p>
 			const writer = new Writer();
-			const b1 = new Element( 'b', { foo: 'bar' } );
-			const b2 = new Element( 'b', { foo: 'baz' } );
-			const p = new Element( 'p', {}, [ b1, b2 ] );
-			const position = new Position( p, 1 );
-			writer.setPriority( b1, 1 );
-			writer.setPriority( b2, 1 );
+			const description = {
+				instanceOf: Element,
+				name: 'p',
+				rangeStart: 0,
+				rangeEnd: 1,
+				children: [
+					{ instanceOf: Text, data: 'foobar' }
+				]
+			};
+
+			const created = create( writer, description );
+			const b = new Element( 'b' );
+			writer.setPriority( b, 1 );
+			const newRange = writer.unwrap( created.range, b );
 
-			const newPosition = writer.mergeAttributes( position );
+			test( writer, newRange, created.node, description );
+		} );
+
+		it( 'should unwrap single node', () => {
+			// <p>[<b>{foobar}</b>]<p> -> <p>[{foobar}]</p>
+			const writer = new Writer();
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				rangeStart: 0,
+				rangeEnd: 1,
+				children: [
+					{
+						instanceOf: Element,
+						priority: 1,
+						name: 'b',
+						children: [
+							{ instanceOf: Text, data: 'foobar' }
+						]
+					}
+				]
+			} );
 
-			expect( newPosition.isEqual( position ) ).to.be.true;
-			expect( p.getChildCount() ).to.equal( 2 );
-			expect( p.getChild( 0 ) ).to.equal( b1 );
-			expect( p.getChild( 1 ) ).to.equal( b2 );
+			const b = new Element( 'b' );
+			writer.setPriority( b, 1 );
+			const newRange = writer.unwrap( created.range, b );
+
+			test( writer, newRange, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				rangeStart: 0,
+				rangeEnd: 1,
+				children: [
+					{ instanceOf: Text, data: 'foobar' }
+				]
+			} );
 		} );
 
-		it( 'should not merge when placed between similar attribute nodes with different priority', () => {
+		it( 'should not unwrap attributes with different priorities #1', () => {
+			// <p>[<b>{foobar}</b>]<p> -> <p>[<b>{foobar}</b>]</p>
 			const writer = new Writer();
-			const b1 = new Element( 'b', { foo: 'bar' } );
-			const b2 = new Element( 'b', { foo: 'bar' } );
-			const p = new Element( 'p', {}, [ b1, b2 ] );
-			const position = new Position( p, 1 );
-			writer.setPriority( b1, 1 );
-			writer.setPriority( b2, 2 );
+			const description =  {
+				instanceOf: Element,
+				name: 'p',
+				rangeStart: 0,
+				rangeEnd: 1,
+				children: [
+					{
+						instanceOf: Element,
+						priority: 1,
+						name: 'b',
+						children: [
+							{ instanceOf: Text, data: 'foobar' }
+						]
+					}
+				]
+			};
+			const created = create( writer, description );
 
-			const newPosition = writer.mergeAttributes( position );
+			const b = new Element( 'b' );
+			writer.setPriority( b, 2 );
+			const newRange = writer.unwrap( created.range, b );
 
-			expect( newPosition.isEqual( position ) ).to.be.true;
-			expect( p.getChildCount() ).to.equal( 2 );
-			expect( p.getChild( 0 ) ).to.equal( b1 );
-			expect( p.getChild( 1 ) ).to.equal( b2 );
+			test( writer, newRange, created.node, description );
 		} );
 
-		it( 'should merge attribute nodes and their contents if possible', () => {
-			// <p><b foo="bar">{foo}</b>|<b foo="bar">{bar}</b></p>
-			// <p><b foo="bar">{foo}|{bar}</b></p>
-			// <p><b foo="bar">{foo|bar}</b></p>
+		it( 'should not unwrap attributes with different priorities #2', () => {
+			// <p>[<b>{foo}</b><b>{bar}</b><b>{baz}</b>]<p> -> <p>[{foo}<b>bar</b>{baz}]</p>
 			const writer = new Writer();
-			const text1 = new Text( 'foo' );
-			const text2 = new Text( 'bar' );
-			const b1 = new Element( 'b', { foo: 'bar' }, [ text1 ] );
-			const b2 = new Element( 'b', { foo: 'bar' }, [ text2 ] );
-			const p = new Element( 'p', {}, [ b1, b2 ] );
-			const position = new Position( p, 1 );
-			writer.setPriority( b1, 1 );
-			writer.setPriority( b2, 1 );
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				rangeStart: 0,
+				rangeEnd: 3,
+				children: [
+					{
+						instanceOf: Element,
+						priority: 2,
+						name: 'b',
+						children: [
+							{ instanceOf: Text, data: 'foo' }
+						]
+					},
+					{
+						instanceOf: Element,
+						priority: 1,
+						name: 'b',
+						children: [
+							{ instanceOf: Text, data: 'bar' }
+						]
+					},
+					{
+						instanceOf: Element,
+						priority: 2,
+						name: 'b',
+						children: [
+							{ instanceOf: Text, data: 'baz' }
+						]
+					}
+				]
+			} );
 
-			const newPosition = writer.mergeAttributes( position );
+			const b = new Element( 'b' );
+			writer.setPriority( b, 2 );
+			const newRange = writer.unwrap( created.range, b );
+
+			test( writer, newRange, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				rangeStart: 0,
+				rangeEnd: 3,
+				children: [
+					{ instanceOf: Text, data: 'foo' },
+					{
+						instanceOf: Element,
+						priority: 1,
+						name: 'b',
+						children: [
+							{ instanceOf: Text, data: 'bar' }
+						]
+					},
+					{ instanceOf: Text, data: 'baz' }
+				]
+			} );
+		} );
 
-			expect( newPosition.parent ).to.equal( text1 );
-			expect( newPosition.offset ).to.equal( 3 );
-			expect( p.getChildCount() ).to.equal( 1 );
-			expect( p.getChild( 0 ) ).to.equal( b1 );
-			expect( b1.getChildCount() ).to.equal( 1 );
-			expect( b1.getChild( 0 ) ).to.equal( text1 );
-			expect( text1.data ).to.equal( 'foobar' );
+		it( 'should unwrap part of the node', () => {
+			// <p>[{baz}<b>{foo]bar}</b><p> -> <p>[{bazfoo}]<b>{bar}</b></p>
+			const writer = new Writer();
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				rangeStart: 0,
+				children: [
+					{ instanceOf: Text, data: 'baz' },
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{ instanceOf: Text, data: 'foobar', rangeEnd: 3 }
+						]
+
+					}
+				]
+			} );
+
+			const b = new Element( 'b' );
+			writer.setPriority( b, 1 );
+
+			const newRange = writer.unwrap( created.range, b );
+
+			test( writer, newRange, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				rangeStart: 0,
+				rangeEnd: 1,
+				children: [
+					{ instanceOf: Text, data: 'bazfoo' },
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{ instanceOf: Text, data: 'bar' }
+						]
+
+					}
+				]
+			} );
+		} );
+
+		it( 'should unwrap nested attributes', () => {
+			// <p>[<u><b>{foobar}</b></u>]</p> -> <p>[<u>{foobar}</u>]</p>
+			const writer = new Writer();
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				rangeStart: 0,
+				rangeEnd: 1,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'u',
+						priority: 1,
+						children: [
+							{
+								instanceOf: Element,
+								name: 'b',
+								priority: 1,
+								children: [
+									{ instanceOf: Text, data: 'foobar' }
+								]
+							}
+						]
+					}
+				]
+			} );
+			const b = new Element( 'b' );
+			writer.setPriority( b, 1 );
+
+			const newRange = writer.unwrap( created.range, b );
+			test( writer, newRange, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				rangeStart: 0,
+				rangeEnd: 1,
+				children: [
+					{
+						instanceOf: Element,
+						name: 'u',
+						priority: 1,
+						children: [
+							{ instanceOf: Text, data: 'foobar' }
+						]
+					}
+				]
+			} );
+		} );
+
+		it( 'should merge unwrapped nodes', () => {
+			// <p>{foo}<u>{bar}</u>[<b><u>{baz]qux}</u></b></p> -> <p>{foo}<u>{bar[baz}</u>]<b><u>{qux}</u></b></p>
+			const writer = new Writer();
+			const created = create( writer, {
+				instanceOf: Element,
+				name: 'p',
+				rangeStart: 2,
+				children: [
+					{ instanceOf: Text, data: 'foo' },
+					{
+						instanceOf: Element,
+						name: 'u',
+						priority: 1,
+						children: [
+							{ instanceOf: Text, data: 'bar' }
+						]
+					},
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{
+								instanceOf: Element,
+								name: 'u',
+								priority: 1,
+								children: [
+									{ instanceOf: Text, data: 'bazqux', rangeEnd: 3 }
+								]
+							}
+						]
+					}
+				]
+			} );
+
+			const b = new Element( 'b' );
+			writer.setPriority( b, 1 );
+			const newRange = writer.unwrap( created.range, b );
+
+			test( writer, newRange, created.node, {
+				instanceOf: Element,
+				name: 'p',
+				rangeEnd: 2,
+				children: [
+					{
+						instanceOf: Text,
+						data: 'foo'
+					},
+					{
+						instanceOf: Element,
+						name: 'u',
+						priority: 1,
+						children: [
+							{ instanceOf: Text, data: 'barbaz', rangeStart: 3 }
+						]
+					},
+					{
+						instanceOf: Element,
+						name: 'b',
+						priority: 1,
+						children: [
+							{
+								instanceOf: Element,
+								name: 'u',
+								priority: 1,
+								children: [
+									{ instanceOf: Text, data: 'qux' }
+								]
+							}
+						]
+					}
+				]
+			} );
 		} );
 	} );
 } );