Răsfoiți Sursa

Merge pull request #814 from ckeditor/t/789

t/789: View range enlarge method
Szymon Cofalik 8 ani în urmă
părinte
comite
b36e08bace

+ 3 - 28
packages/ckeditor5-engine/src/conversion/model-to-view-converters.js

@@ -7,12 +7,8 @@ import ModelRange from '../model/range';
 import ModelPosition from '../model/position';
 import ModelElement from '../model/element';
 
-import ViewRange from '../view/range';
 import ViewElement from '../view/element';
-import ViewAttributeElement from '../view/element';
-import ViewUIElement from '../view/element';
 import ViewText from '../view/text';
-import ViewTreeWalker from '../view/treewalker';
 import viewWriter from '../view/writer';
 
 /**
@@ -414,27 +410,6 @@ export function unwrapRange( elementCreator ) {
 	};
 }
 
-// Takes given `viewPosition` and returns a widest possible range that contains this position and all view elements
-// before that position and after that position which has zero length in model (empty `ViewAttributeElement`s and `ViewUIElement`s).
-// @param {module:engine/view/position~Position} viewPosition Position to start from when looking for furthest zero length position.
-// @returns {module:engine/view/range~Range}
-function enlargeViewPosition( viewPosition ) {
-	const start = viewPosition;
-	let end = viewPosition;
-
-	const walker = new ViewTreeWalker( { startPosition: start } );
-
-	for ( let step of walker ) {
-		if ( step.item instanceof ViewAttributeElement || step.item instanceof ViewUIElement ) {
-			end = walker.position;
-		} else {
-			break;
-		}
-	}
-
-	return new ViewRange( start, end );
-}
-
 /**
  * Function factory, creates a default model-to-view converter for nodes move changes.
  *
@@ -500,10 +475,10 @@ export function removeUIElement( elementCreator ) {
 			return;
 		}
 
-		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
-		const viewRange = enlargeViewPosition( viewPosition );
+		const viewRange = conversionApi.mapper.toViewRange( data.range );
+		const enlargedViewRange = viewRange.getEnlarged();
 
-		viewWriter.clear( viewRange, viewElement );
+		viewWriter.clear( enlargedViewRange, viewElement );
 	};
 }
 

+ 31 - 0
packages/ckeditor5-engine/src/model/position.js

@@ -9,6 +9,7 @@
 
 import DocumentFragment from './documentfragment';
 import Element from './element';
+import TreeWalker from './treewalker';
 import last from '@ckeditor/ckeditor5-utils/src/lib/lodash/last';
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -244,6 +245,36 @@ export default class Position {
 		}
 	}
 
+	/**
+	 * Gets the farthest position which matches the callback using
+	 * {@link module:engine/model/treewalker~TreeWalker TreeWalker}.
+	 *
+	 * For example:
+	 *
+	 * 		getLastMatchingPosition( value => value.type == 'text' );
+	 * 		// <paragraph>[]foo</paragraph> -> <paragraph>foo[]</paragraph>
+	 *
+	 * 		getLastMatchingPosition( value => value.type == 'text', { direction: 'backward' } );
+	 * 		// <paragraph>foo[]</paragraph> -> <paragraph>[]foo</paragraph>
+	 *
+	 * 		getLastMatchingPosition( value => false );
+	 * 		// Do not move the position.
+	 *
+	 * @param {Function} skip Callback function. Gets {@link module:engine/model/treewalker~TreeWalkerValue} and should
+	 * return `true` if the value should be skipped or `false` if not.
+	 * @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
+	 *
+	 * @returns {module:engine/model/position~Position} The position after the last item which matches the `skip` callback test.
+	 */
+	getLastMatchingPosition( skip, options = {} ) {
+		options.startPosition = this;
+
+		const treeWalker = new TreeWalker( options );
+		treeWalker.skip( skip );
+
+		return treeWalker.position;
+	}
+
 	/**
 	 * Returns a path to this position's parent. Parent path is equal to position {@link module:engine/model/position~Position#path path}
 	 * but without the last item.

+ 28 - 0
packages/ckeditor5-engine/src/model/treewalker.js

@@ -152,6 +152,34 @@ export default class TreeWalker {
 		return this;
 	}
 
+	/**
+	 * Moves {@link #position} in the {@link #direction} skipping values as long as the callback function returns `true`.
+	 *
+	 * For example:
+	 *
+	 * 		walker.skip( value => value.type == 'text' ); // <paragraph>[]foo</paragraph> -> <paragraph>foo[]</paragraph>
+	 * 		walker.skip( value => true ); // Move the position to the end: <paragraph>[]foo</paragraph> -> <paragraph>foo</paragraph>[]
+	 * 		walker.skip( value => false ); // Do not move the position.
+	 *
+	 * @param {Function} skip Callback function. Gets {@link module:engine/model/treewalker~TreeWalkerValue} and should
+	 * return `true` if the value should be skipped or `false` if not.
+	 */
+	skip( skip ) {
+		let done, value, prevPosition, prevVisitedParent;
+
+		do {
+			prevPosition = this.position;
+			prevVisitedParent = this._visitedParent;
+
+			( { done, value } = this.next() );
+		} while ( !done && skip( value ) );
+
+		if ( !done ) {
+			this.position = prevPosition;
+			this._visitedParent = prevVisitedParent;
+		}
+	}
+
 	/**
 	 * Iterator interface method.
 	 * Detects walking direction and makes step forward or backward.

+ 27 - 0
packages/ckeditor5-engine/src/view/position.js

@@ -11,6 +11,8 @@ import Text from './text';
 import TextProxy from './textproxy';
 import DocumentFragment from './documentfragment';
 
+import TreeWalker from './treewalker';
+
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import EditableElement from './editableelement';
@@ -139,6 +141,31 @@ export default class Position {
 		return shifted;
 	}
 
+	/**
+	 * Gets the farthest position which matches the callback using
+	 * {@link module:engine/view/treewalker~TreeWalker TreeWalker}.
+	 *
+	 * For example:
+	 *
+	 * 		getLastMatchingPosition( value => value.type == 'text' ); // <p>{}foo</p> -> <p>foo[]</p>
+	 * 		getLastMatchingPosition( value => value.type == 'text', { direction: 'backward' } ); // <p>foo[]</p> -> <p>{}foo</p>
+	 * 		getLastMatchingPosition( value => false ); // Do not move the position.
+	 *
+	 * @param {Function} skip Callback function. Gets {@link module:engine/view/treewalker~TreeWalkerValue} and should
+	 * return `true` if the value should be skipped or `false` if not.
+	 * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
+	 *
+	 * @returns {module:engine/view/position~Position} The position after the last item which matches the `skip` callback test.
+	 */
+	getLastMatchingPosition( skip, options = {} ) {
+		options.startPosition = this;
+
+		const treeWalker = new TreeWalker( options );
+		treeWalker.skip( skip );
+
+		return treeWalker.position;
+	}
+
 	/**
 	 * Returns ancestors array of this position, that is this position's parent and it's ancestors.
 	 *

+ 88 - 0
packages/ckeditor5-engine/src/view/range.js

@@ -7,9 +7,14 @@
  * @module engine/view/range
  */
 
+import Text from './text';
 import Position from './position';
 import TreeWalker from './treewalker';
 
+import AttributeElement from './attributeelement';
+import ContainerElement from './containerelement';
+import UIElement from './uielement';
+
 /**
  * Tree view range.
  */
@@ -81,6 +86,63 @@ export default class Range {
 		return this.start.root;
 	}
 
+	/**
+	 * Creates a maximal range that has the same content as this range but is expanded in both ways (at the beginning
+	 * and at the end).
+	 *
+	 * For example:
+	 *
+	 * 		<p>Foo</p><p><b>{Bar}</b></p> -> <p>Foo</p>[<p><b>Bar</b></p>]
+	 * 		<p><b>foo</b>{bar}<span></span></p> -> <p><b>foo[</b>bar<span></span>]</p>
+	 *
+	 * Note that in the sample above:
+	 *  - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
+	 *  - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
+	 *  - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
+	 *
+	 * @returns {module:engine/view/range~Range} Enlarged range.
+	 */
+	getEnlarged() {
+		const start = this.start.getLastMatchingPosition( enlargeShrinkStartSkip, { direction: 'backward' } );
+		const end = this.end.getLastMatchingPosition( enlargeShrinkEndSkip );
+
+		return new Range( start, end );
+	}
+
+	/**
+	 * Creates a minimum range that has the same content as this range but is trimmed in both ways (at the beginning
+	 * and at the end).
+	 *
+	 * For example:
+	 *
+	 * 		<p>Foo</p>[<p><b>Bar</b></p>] -> <p>Foo</p><p><b>{Bar}</b></p>
+	 * 		<p><b>foo[</b>bar<span></span>]</p> -> <p><b>foo</b>{bar}<span></span></p>
+	 *
+	 * Note that in the sample above:
+	 *  - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
+	 *  - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
+	 *  - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
+	 *
+	 * @returns {module:engine/view/range~Range} Shrink range.
+	 */
+	getTrimmed() {
+		let start = this.start.getLastMatchingPosition( enlargeShrinkStartSkip );
+		let end = this.end.getLastMatchingPosition( enlargeShrinkEndSkip, { direction: 'backward' } );
+		let nodeAfterStart = start.nodeAfter;
+		let nodeBeforeEnd = end.nodeBefore;
+
+		// Because TreeWalker prefers positions next to text node, we need to move them manually into these text nodes.
+		if ( nodeAfterStart instanceof Text ) {
+			start = new Position( nodeAfterStart, 0 );
+		}
+
+		if ( nodeBeforeEnd instanceof Text ) {
+			end = new Position( nodeBeforeEnd, nodeBeforeEnd.data.length );
+		}
+
+		return new Range( start, end );
+	}
+
 	/**
 	 * Two ranges are equal if their start and end positions are equal.
 	 *
@@ -349,3 +411,29 @@ export default class Range {
 		return this.createFromPositionAndShift( Position.createBefore( item ), 1 );
 	}
 }
+
+// Function used by getEnlagred and getShrinked methods.
+function enlargeShrinkStartSkip( value ) {
+	if ( value.item instanceof AttributeElement || value.item instanceof UIElement ) {
+		return true;
+	}
+
+	if ( value.item instanceof ContainerElement && value.type == 'elementStart' ) {
+		return true;
+	}
+
+	return false;
+}
+
+// Function used by getEnlagred and getShrinked methods.
+function enlargeShrinkEndSkip( value ) {
+	if ( value.item instanceof AttributeElement || value.item instanceof UIElement ) {
+		return true;
+	}
+
+	if ( value.item instanceof ContainerElement && value.type == 'elementEnd' ) {
+		return true;
+	}
+
+	return false;
+}

+ 26 - 0
packages/ckeditor5-engine/src/view/treewalker.js

@@ -140,6 +140,32 @@ export default class TreeWalker {
 		return this;
 	}
 
+	/**
+	 * Moves {@link #position} in the {@link #direction} skipping values as long as the callback function returns `true`.
+	 *
+	 * For example:
+	 *
+	 * 		walker.skip( value => value.type == 'text' ); // <p>{}foo</p> -> <p>foo[]</p>
+	 * 		walker.skip( value => true ); // Move the position to the end: <p>{}foo</p> -> <p>foo</p>[]
+	 * 		walker.skip( value => false ); // Do not move the position.
+	 *
+	 * @param {Function} skip Callback function. Gets {@link module:engine/view/treewalker~TreeWalkerValue} and should
+	 * return `true` if the value should be skipped or `false` if not.
+	 */
+	skip( skip ) {
+		let done, value, prevPosition;
+
+		do {
+			prevPosition = this.position;
+
+			( { done, value } = this.next() );
+		} while ( !done && skip( value ) );
+
+		if ( !done ) {
+			this.position = prevPosition;
+		}
+	}
+
 	/**
 	 * Iterator interface method.
 	 * Detects walking direction and makes step forward or backward.

+ 18 - 0
packages/ckeditor5-engine/tests/model/position.js

@@ -578,6 +578,24 @@ describe( 'position', () => {
 		} );
 	} );
 
+	describe( 'getLastMatchingPosition', () => {
+		it( 'should skip forward', () => {
+			let position = new Position( root, [ 1, 0, 0 ] );
+
+			position = position.getLastMatchingPosition( ( value ) => value.type == 'text' );
+
+			expect( position.path ).to.deep.equal( [ 1, 0, 3 ] );
+		} );
+
+		it( 'should skip backward', () => {
+			let position = new Position( root, [ 1, 0, 2 ] );
+
+			position = position.getLastMatchingPosition( ( value ) => value.type == 'text', { direction: 'backward' } );
+
+			expect( position.path ).to.deep.equal( [ 1, 0, 0 ] );
+		} );
+	} );
+
 	describe( '_getTransformedByInsertion', () => {
 		it( 'should return a new Position instance', () => {
 			const position = new Position( root, [ 0 ] );

+ 75 - 0
packages/ckeditor5-engine/tests/model/treewalker.js

@@ -562,6 +562,81 @@ describe( 'TreeWalker', () => {
 			expectValue( value, expected[ i++ ], { ignoreElementEnd: true } );
 		}
 	} );
+
+	describe( 'skip', () => {
+		describe( 'forward treewalker', () => {
+			it( 'should jump over all text nodes', () => {
+				const walker = new TreeWalker( {
+					startPosition: Position.createFromParentAndOffset( paragraph, 0 )
+				} );
+
+				walker.skip( value => value.type == 'text' );
+
+				expect( walker.position.parent ).to.equal( paragraph );
+				expect( walker.position.offset ).to.equal( 3 );
+			} );
+
+			it( 'should do not move if the condition is false', () => {
+				const walker = new TreeWalker( {
+					startPosition: Position.createFromParentAndOffset( paragraph, 1 )
+				} );
+
+				walker.skip( () => false );
+
+				expect( walker.position.parent ).to.equal( paragraph );
+				expect( walker.position.offset ).to.equal( 1 );
+			} );
+
+			it( 'should move to the end if the condition is true', () => {
+				const walker = new TreeWalker( {
+					startPosition: Position.createFromParentAndOffset( paragraph, 1 )
+				} );
+
+				walker.skip( () => true );
+
+				expect( walker.position.parent ).to.equal( rootEnding.parent );
+				expect( walker.position.offset ).to.equal( rootEnding.offset );
+			} );
+		} );
+
+		describe( 'backward treewalker', () => {
+			it( 'should jump over all text nodes', () => {
+				const walker = new TreeWalker( {
+					startPosition: Position.createFromParentAndOffset( paragraph, 3 ),
+					direction: 'backward'
+				} );
+
+				walker.skip( value => value.type == 'text' );
+
+				expect( walker.position.parent ).to.equal( paragraph );
+				expect( walker.position.offset ).to.equal( 0 );
+			} );
+
+			it( 'should do not move if the condition is false', () => {
+				const walker = new TreeWalker( {
+					startPosition: Position.createFromParentAndOffset( paragraph, 1 ),
+					direction: 'backward'
+				} );
+
+				walker.skip( () => false );
+
+				expect( walker.position.parent ).to.equal( paragraph );
+				expect( walker.position.offset ).to.equal( 1 );
+			} );
+
+			it( 'should move to the end if the condition is true', () => {
+				const walker = new TreeWalker( {
+					startPosition: Position.createFromParentAndOffset( paragraph, 1 ),
+					direction: 'backward'
+				} );
+
+				walker.skip( () => true );
+
+				expect( walker.position.parent ).to.equal( rootBeginning.parent );
+				expect( walker.position.offset ).to.equal( rootBeginning.offset );
+			} );
+		} );
+	} );
 } );
 
 function expectValue( value, expected, options ) {

+ 21 - 1
packages/ckeditor5-engine/tests/view/position.js

@@ -14,7 +14,7 @@ import TextProxy from '../../src/view/textproxy';
 
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
-import { parse } from '../../src/dev-utils/view';
+import { parse, stringify } from '../../src/dev-utils/view';
 
 describe( 'Position', () => {
 	const parentMock = {};
@@ -99,6 +99,26 @@ describe( 'Position', () => {
 		} );
 	} );
 
+	describe( 'getLastMatchingPosition', () => {
+		it( 'should skip forward', () => {
+			const { view, selection } = parse( '<p><b>{}foo</b></p>' );
+			let position = selection.getFirstPosition();
+
+			position = position.getLastMatchingPosition( ( value ) => value.type == 'text' );
+
+			expect( stringify( view, position ) ).to.equal( '<p><b>foo[]</b></p>' );
+		} );
+
+		it( 'should skip backward', () => {
+			const { view, selection } = parse( '<p><b>foo{}</b></p>' );
+			let position = selection.getFirstPosition();
+
+			position = position.getLastMatchingPosition( ( value ) => value.type == 'text', { direction: 'backward' } );
+
+			expect( stringify( view, position ) ).to.equal( '<p><b>[]foo</b></p>' );
+		} );
+	} );
+
 	describe( 'getRoot', () => {
 		it( 'should return it\'s parent root', () => {
 			const foo = new Text( 'foo' );

+ 106 - 1
packages/ckeditor5-engine/tests/view/range.js

@@ -9,7 +9,7 @@ import Element from '../../src/view/element';
 import DocumentFragment from '../../src/view/documentfragment';
 import Text from '../../src/view/text';
 import TreeWalker from '../../src/view/treewalker';
-import { parse } from '../../src/dev-utils/view';
+import { parse, stringify } from '../../src/dev-utils/view';
 
 function getRange( view, options = {} ) {
 	const { selection } = parse( view, options );
@@ -86,6 +86,111 @@ describe( 'Range', () => {
 		} );
 	} );
 
+	describe( 'getEnlarged', () => {
+		it( 'case 1', () => {
+			expect( enlarge( '<p>f<b>{oo}</b></p><p>bar</p>' ) )
+				.to.equal( '<p>f[<b>oo</b></p>]<p>bar</p>' );
+		} );
+
+		it( 'case 2', () => {
+			expect( enlarge( '<p>f{oo}bar</p>' ) )
+				.to.equal( '<p>f{oo}bar</p>' );
+		} );
+
+		it( 'case 3', () => {
+			expect( enlarge( '<p>f<span></span>{oo}<span></span>bar</p>' ) )
+				.to.equal( '<p>f[<span></span>oo<span></span>]bar</p>' );
+		} );
+
+		it( 'case 4', () => {
+			expect( enlarge( '<p>f<img></img>{oo}<img></img>bar</p>' ) )
+				.to.equal( '<p>f<img></img>[oo]<img></img>bar</p>' );
+		} );
+
+		it( 'case 5', () => {
+			expect( enlarge( '<p><b>f</b>{oo}<b><span></span>bar</b></p>' ) )
+				.to.equal( '<p><b>f[</b>oo<b><span></span>]bar</b></p>' );
+		} );
+
+		it( 'case6', () => {
+			expect( enlarge( '<p>foo</p><p>[bar]</p><p>bom</p>' ) )
+				.to.equal( '<p>foo</p>[<p>bar</p>]<p>bom</p>' );
+		} );
+
+		function enlarge( data ) {
+			data = data
+				.replace( /<p>/g, '<container:p>' )
+				.replace( /<\/p>/g, '</container:p>' )
+				.replace( /<b>/g, '<attribute:b>' )
+				.replace( /<\/b>/g, '</attribute:b>' )
+				.replace( /<img><\/img>/g, '<empty:img></empty:img>' )
+				.replace( /<span><\/span>/g, '<ui:span></ui:span>' );
+
+			const viewFrag = new DocumentFragment();
+			const { view, selection } = parse( data, { rootElement: viewFrag } );
+			const range = selection.getFirstRange();
+
+			const enlargedRange = range.getEnlarged();
+
+			return stringify( view, enlargedRange );
+		}
+	} );
+
+	describe( 'getTrimmed', () => {
+		it( 'case 1', () => {
+			expect( trim( '<p>f[<b>oo</b></p>]<p>bar</p>' ) )
+				.to.equal( '<p>f<b>{oo}</b></p><p>bar</p>' );
+		} );
+
+		it( 'case 2', () => {
+			expect( trim( '<p>f{oo}bar</p>' ) )
+				.to.equal( '<p>f{oo}bar</p>' );
+		} );
+
+		it( 'case 3', () => {
+			expect( trim( '<p>f[<span></span>oo<span></span>]bar</p>' ) )
+				.to.equal( '<p>f<span></span>{oo}<span></span>bar</p>' );
+		} );
+
+		it( 'case 4', () => {
+			expect( trim( '<p>f<img></img>[oo]<img></img>bar</p>' ) )
+				.to.equal( '<p>f<img></img>{oo}<img></img>bar</p>' );
+		} );
+
+		it( 'case 5', () => {
+			expect( trim( '<p><b>f[</b>oo<b><span></span>]bar</b></p>' ) )
+				.to.equal( '<p><b>f</b>{oo}<b><span></span>bar</b></p>' );
+		} );
+
+		it( 'case6', () => {
+			expect( trim( '<p>foo[</p><p>bar</p><p>]bom</p>' ) )
+				.to.equal( '<p>foo[</p><p>bar</p><p>]bom</p>' );
+		} );
+
+		it( 'case7', () => {
+			expect( trim( '<p>foo[<b><img></img></b>]bom</p>' ) )
+				.to.equal( '<p>foo<b>[<img></img>]</b>bom</p>' );
+		} );
+
+		function trim( data ) {
+			data = data
+				.replace( /<p>/g, '<container:p>' )
+				.replace( /<\/p>/g, '</container:p>' )
+				.replace( /<b>/g, '<attribute:b>' )
+				.replace( /<\/b>/g, '</attribute:b>' )
+				.replace( /<img><\/img>/g, '<empty:img></empty:img>' )
+				.replace( /<span><\/span>/g, '<ui:span></ui:span>' );
+
+			const viewFrag = new DocumentFragment();
+			const { view, selection } = parse( data, { rootElement: viewFrag } );
+			const range = selection.getFirstRange();
+
+			const trimmedRange = range.getTrimmed();
+
+			return stringify( view, trimmedRange );
+		}
+	} );
+
 	describe( 'isEqual', () => {
 		it( 'should return true for the same range', () => {
 			const start = new Position( {}, 1 );

+ 87 - 4
packages/ckeditor5-engine/tests/view/treewalker.js

@@ -27,10 +27,7 @@ describe( 'TreeWalker', () => {
 		//  |- img1
 		//  |- p
 		//     |- b
-		//     |  |- A
-		//     |  |- B
-		//     |  |- C
-		//     |  |- D
+		//     |  |- abcd
 		//     |
 		//     |- Y
 		//     |
@@ -1006,6 +1003,92 @@ describe( 'TreeWalker', () => {
 
 		expect( nodes ).to.deep.equal( [ p, foo, b, bar ] );
 	} );
+
+	describe( 'skip', () => {
+		describe( 'forward treewalker', () => {
+			it( 'should jump over all text nodes', () => {
+				const walker = new TreeWalker( {
+					startPosition: new Position( paragraph, 0 )
+				} );
+
+				walker.skip( value => value.type == 'text' || value.item.name == 'b' );
+
+				expect( walker.position.parent ).to.equal( paragraph );
+				expect( walker.position.offset ).to.equal( 2 );
+			} );
+
+			it( 'should do not move if the condition is false', () => {
+				const walker = new TreeWalker( {
+					startPosition: new Position( bold, 0 )
+				} );
+
+				walker.skip( () => false );
+
+				expect( walker.position.parent ).to.equal( bold );
+				expect( walker.position.offset ).to.equal( 0 );
+			} );
+
+			it( 'should do not move if the condition is false and the position is in text node', () => {
+				const walker = new TreeWalker( {
+					startPosition: new Position( bold.getChild( 0 ), 2 )
+				} );
+
+				walker.skip( () => false );
+
+				expect( walker.position.parent ).to.equal( bold.getChild( 0 ) );
+				expect( walker.position.offset ).to.equal( 2 );
+			} );
+
+			it( 'should move to the end if the condition is true', () => {
+				const walker = new TreeWalker( {
+					startPosition: new Position( bold, 0 )
+				} );
+
+				walker.skip( () => true );
+
+				expect( walker.position.parent ).to.equal( rootEnding.parent );
+				expect( walker.position.offset ).to.equal( rootEnding.offset );
+			} );
+		} );
+
+		describe( 'backward treewalker', () => {
+			it( 'should jump over all text nodes', () => {
+				const walker = new TreeWalker( {
+					startPosition: new Position( bold.getChild( 0 ), 2 ),
+					direction: 'backward'
+				} );
+
+				walker.skip( value => value.type == 'text' || value.item.name == 'b' );
+
+				expect( walker.position.parent ).to.equal( paragraph );
+				expect( walker.position.offset ).to.equal( 0 );
+			} );
+
+			it( 'should do not move if the condition is false', () => {
+				const walker = new TreeWalker( {
+					startPosition: new Position( bold, 0 ),
+					direction: 'backward'
+				} );
+
+				walker.skip( () => false );
+
+				expect( walker.position.parent ).to.equal( bold );
+				expect( walker.position.offset ).to.equal( 0 );
+			} );
+
+			it( 'should move to the end if the condition is true', () => {
+				const walker = new TreeWalker( {
+					startPosition: new Position( bold, 0 ),
+					direction: 'backward'
+				} );
+
+				walker.skip( () => true );
+
+				expect( walker.position.parent ).to.equal( rootBeginning.parent );
+				expect( walker.position.offset ).to.equal( rootBeginning.offset );
+			} );
+		} );
+	} );
 } );
 
 function expectValue( value, expected, options = {} ) {