8
0
Просмотр исходного кода

Introduced view.Range#getEnlarded, view.Range#getShrinked, view.Position#getFurtherPosition, view.Position#getPriorPosition, view.TreeWalker#skip.

Piotr Jasiun 9 лет назад
Родитель
Сommit
625c85b055

+ 40 - 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,44 @@ export default class Position {
 		return shifted;
 	}
 
+	/**
+	 * Use forward {@link module:engine/view/treewalker~TreeWalker TreeWalker} to get the farthest position which
+	 * matches the callback.
+	 *
+	 * For example:
+	 *
+	 * 		getFurtherPosition( value => value.type == 'text' ); // <p>{}foo</p> -> <p>foo[]</p>
+	 * 		getFurtherPosition( 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.
+	 */
+	getFurtherPosition( skip ) {
+		const treeWalker = new TreeWalker( { startPosition: this } );
+		treeWalker.skip( skip );
+
+		return treeWalker.position;
+	}
+
+	/**
+	 * Use backward {@link module:engine/view/treewalker~TreeWalker TreeWalker} to get the farthest position which
+	 * matches the callback.
+	 *
+	 * For example:
+	 *
+	 * 		getPriorPosition( value => value.type == 'text' ); // <p>[]foo</p> -> <p>foo{}</p>
+	 * 		getPriorPosition( 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.
+	 */
+	getPriorPosition( skip ) {
+		const treeWalker = new TreeWalker( { startPosition: this, direction: 'backward' } );
+		treeWalker.skip( skip );
+
+		return treeWalker.position;
+	}
+
 	/**
 	 * Returns ancestors array of this position, that is this position's parent and it's ancestors.
 	 *

+ 85 - 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 './AttributeElement';
+import UIElement from './uielement';
+
 /**
  * Tree view range.
  */
@@ -81,6 +86,60 @@ export default class Range {
 		return this.start.root;
 	}
 
+	/**
+	 * Create a new range with the same content, but containing all surrendering tags with no semantic meaning.
+	 *
+	 * 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 all:
+	 *  - `<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.
+	 */
+	getEnlagred() {
+		const start = this.start.getPriorPosition( enlagreShrinkStartSkip );
+		const end = this.end.getFurtherPosition( enlagreShrinkEndSkip );
+
+		return new Range( start, end );
+	}
+
+	/**
+	 * Create a new range with the same content, but without all surrendering tags with no semantic meaning.
+	 *
+	 * 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 all:
+	 *  - `<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.
+	 */
+	getShrinked() {
+		let start = this.start.getFurtherPosition( enlagreShrinkStartSkip );
+		let end = this.end.getPriorPosition( enlagreShrinkEndSkip );
+		let nodeAfterStart = start.nodeAfter;
+		let nodeBeforeEnd = end.nodeBefore;
+
+		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 +408,29 @@ export default class Range {
 		return this.createFromPositionAndShift( Position.createBefore( item ), 1 );
 	}
 }
+
+// Function used by getEnlagred and getShrinked methods.
+function enlagreShrinkStartSkip( 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 enlagreShrinkEndSkip( 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;
 	}
 
+	/**
+	 * Move {@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.

+ 23 - 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,28 @@ describe( 'Position', () => {
 		} );
 	} );
 
+	describe( 'getFurtherPosition', () => {
+		it( 'should return skip', () => {
+			const { view, selection } = parse( '<p><b>{}foo</b></p>' );
+			let position = selection.getFirstPosition();
+
+			position = position.getFurtherPosition( ( value ) => value.type == 'text' );
+
+			expect( stringify( view, position ) ).to.equal( '<p><b>foo[]</b></p>' );
+		} );
+	} );
+
+	describe( 'getPriorPosition', () => {
+		it( 'should return skip', () => {
+			const { view, selection } = parse( '<p><b>foo{}</b></p>' );
+			let position = selection.getFirstPosition();
+
+			position = position.getPriorPosition( ( value ) => value.type == 'text' );
+
+			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( 'getEnlagred', () => {
+		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.getEnlagred();
+
+			return stringify( view, enlargedRange );
+		}
+	} );
+
+	describe( 'getShrinked', () => {
+		it( 'case 1', () => {
+			expect( shrink( '<p>f[<b>oo</b></p>]<p>bar</p>' ) )
+				.to.equal( '<p>f<b>{oo}</b></p><p>bar</p>' );
+		} );
+
+		it( 'case 2', () => {
+			expect( shrink( '<p>f{oo}bar</p>' ) )
+				.to.equal( '<p>f{oo}bar</p>' );
+		} );
+
+		it( 'case 3', () => {
+			expect( shrink( '<p>f[<span></span>oo<span></span>]bar</p>' ) )
+				.to.equal( '<p>f<span></span>{oo}<span></span>bar</p>' );
+		} );
+
+		it( 'case 4', () => {
+			expect( shrink( '<p>f<img></img>[oo]<img></img>bar</p>' ) )
+				.to.equal( '<p>f<img></img>{oo}<img></img>bar</p>' );
+		} );
+
+		it( 'case 5', () => {
+			expect( shrink( '<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( shrink( '<p>foo[</p><p>bar</p><p>]bom</p>' ) )
+				.to.equal( '<p>foo[</p><p>bar</p><p>]bom</p>' );
+		} );
+
+		it( 'case7', () => {
+			expect( shrink( '<p>foo[<b><img></img></b>]bom</p>' ) )
+				.to.equal( '<p>foo<b>[<img></img>]</b>bom</p>' );
+		} );
+
+		function shrink( 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 shrinkedRange = range.getShrinked();
+
+			return stringify( view, shrinkedRange );
+		}
+	} );
+
 	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 = {} ) {