| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 |
- /**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- import Element from './element.js';
- import Text from './text.js';
- import TextProxy from './textproxy.js';
- import Position from './position.js';
- import CKEditorError from '../../utils/ckeditorerror.js';
- /**
- * Position iterator class. It allows to iterate forward and backward over the document.
- *
- * @memberOf engine.view
- */
- export default class TreeWalker {
- /**
- * Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or `startPosition`.
- *
- * @constructor
- * @param {Object} options Object with configuration.
- * @param {engine.view.Range} [options.boundaries=null] Range to define boundaries of the iterator.
- * @param {engine.view.Position} [options.startPosition] Starting position.
- * @param {'forward'|'backward'} [options.direction='forward'] Walking direction.
- * @param {Boolean} [options.singleCharacters=false] Flag indicating whether all characters from
- * {@link engine.view.Text} should be returned as one {@link engine.view.Text} (`false`) ore one by one as
- * {@link engine.view.TextProxy} (`true`).
- * @param {Boolean} [options.shallow=false] Flag indicating whether iterator should enter elements or not. If the
- * iterator is shallow child nodes of any iterated node will not be returned along with `elementEnd` tag.
- * @param {Boolean} [options.ignoreElementEnd=false] Flag indicating whether iterator should ignore `elementEnd`
- * tags. If the option is true walker will not return a parent node of start position. If this option is `true`
- * each {@link engine.view.Element} will be returned once, while if the option is `false` they might be returned
- * twice: for `'elementStart'` and `'elementEnd'`.
- */
- constructor( options = {} ) {
- if ( !options.boundaries && !options.startPosition ) {
- /**
- * Neither boundaries nor starting position have been defined.
- *
- * @error tree-walker-no-start-position
- */
- throw new CKEditorError( 'view-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
- }
- if ( options.direction && options.direction != 'forward' && options.direction != 'backward' ) {
- throw new CKEditorError(
- 'view-tree-walker-unknown-direction: Only `backward` and `forward` direction allowed.',
- { direction: options.direction }
- );
- }
- /**
- * Iterator boundaries.
- *
- * When the iterator is walking `'forward'` on the end of boundary or is walking `'backward'`
- * on the start of boundary, then `{ done: true }` is returned.
- *
- * If boundaries are not defined they are set before first and after last child of the root node.
- *
- * @readonly
- * @member {engine.view.Range} engine.view.TreeWalker#boundaries
- */
- this.boundaries = options.boundaries || null;
- /**
- * Iterator position. If start position is not defined then position depends on {@link #direction}. If direction is
- * `'forward'` position starts form the beginning, when direction is `'backward'` position starts from the end.
- *
- * @readonly
- * @member {engine.view.Position} engine.view.TreeWalker#position
- */
- if ( options.startPosition ) {
- this.position = Position.createFromPosition( options.startPosition );
- } else {
- this.position = Position.createFromPosition( options.boundaries[ options.direction == 'backward' ? 'end' : 'start' ] );
- }
- /**
- * Walking direction. Defaults `'forward'`.
- *
- * @readonly
- * @member {'backward'|'forward'} engine.view.TreeWalker#direction
- */
- this.direction = options.direction || 'forward';
- /**
- * Flag indicating whether all characters from {@link engine.view.Text} should be returned as one
- * {@link engine.view.Text} or one by one as {@link engine.view.TextProxy}.
- *
- * @readonly
- * @member {Boolean} engine.view.TreeWalker#singleCharacters
- */
- this.singleCharacters = !!options.singleCharacters;
- /**
- * Flag indicating whether iterator should enter elements or not. If the iterator is shallow child nodes of any
- * iterated node will not be returned along with `elementEnd` tag.
- *
- * @readonly
- * @member {Boolean} engine.view.TreeWalker#shallow
- */
- this.shallow = !!options.shallow;
- /**
- * Flag indicating whether iterator should ignore `elementEnd` tags. If set to `true`, walker will not
- * return a parent node of the start position. Each {@link engine.view.Element} will be returned once.
- * When set to `false` each element might be returned twice: for `'elementStart'` and `'elementEnd'`.
- *
- * @readonly
- * @member {Boolean} engine.view.TreeWalker#ignoreElementEnd
- */
- this.ignoreElementEnd = !!options.ignoreElementEnd;
- /**
- * Start boundary parent.
- *
- * @private
- * @member {engine.view.Node} engine.view.TreeWalker#_boundaryStartParent
- */
- this._boundaryStartParent = this.boundaries ? this.boundaries.start.parent : null;
- /**
- * End boundary parent.
- *
- * @private
- * @member {engine.view.Node} engine.view.TreeWalker#_boundaryEndParent
- */
- this._boundaryEndParent = this.boundaries ? this.boundaries.end.parent : null;
- }
- /**
- * Iterator interface.
- */
- [ Symbol.iterator ]() {
- return this;
- }
- /**
- * Iterator interface method.
- * Detects walking direction and makes step forward or backward.
- *
- * @returns {Object} Object implementing iterator interface, returning information about taken step.
- */
- next() {
- if ( this.direction == 'forward' ) {
- return this._next();
- } else {
- return this._previous();
- }
- }
- /**
- * Makes a step forward in view. Moves the {@link #position} to the next position and returns the encountered value.
- *
- * @private
- * @returns {Object}
- * @returns {Boolean} return.done `true` if iterator is done, `false` otherwise.
- * @returns {engine.view.TreeWalkerValue} return.value Information about taken step.
- */
- _next() {
- let position = Position.createFromPosition( this.position );
- const previousPosition = this.position;
- const parent = position.parent;
- // We are at the end of the root.
- if ( parent.parent === null && position.offset === parent.childCount ) {
- return { done: true };
- }
- // We reached the walker boundary.
- if ( parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset ) {
- return { done: true };
- }
- // Get node just after current position.
- let node;
- // Text is a specific parent because it contains string instead of child nodes.
- if ( parent instanceof Text ) {
- node = parent.data[ position.offset ];
- } else {
- node = parent.getChild( position.offset );
- }
- if ( node instanceof Element ) {
- if ( !this.shallow ) {
- position = new Position( node, 0 );
- } else {
- position.offset++;
- }
- this.position = position;
- return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
- } else if ( node instanceof Text ) {
- if ( this.singleCharacters ) {
- position = new Position( node, 0 );
- this.position = position;
- return this._next();
- } else {
- let charactersCount = node.data.length;
- let item = node;
- // If text stick out of walker range, we need to cut it and wrap by TextProxy.
- if ( node == this._boundaryEndParent ) {
- charactersCount = this.boundaries.end.offset;
- item = new TextProxy( node, 0, charactersCount );
- position = Position.createAfter( item );
- } else {
- // If not just keep moving forward.
- position.offset++;
- }
- this.position = position;
- return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount );
- }
- } else if ( typeof node == 'string' ) {
- let textLength;
- if ( this.singleCharacters ) {
- textLength = 1;
- } else {
- // Check if text stick out of walker range.
- const endOffset = parent === this._boundaryEndParent ? this.boundaries.end.offset : parent.data.length;
- textLength = endOffset - position.offset;
- }
- const textProxy = new TextProxy( parent, position.offset, textLength );
- position.offset += textLength;
- this.position = position;
- return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
- } else {
- // `node` is not set, we reached the end of current `parent`.
- position = Position.createAfter( parent );
- this.position = position;
- if ( this.ignoreElementEnd ) {
- return this._next();
- } else {
- return this._formatReturnValue( 'elementEnd', parent, previousPosition, position );
- }
- }
- }
- /**
- * Makes a step backward in view. Moves the {@link #position} to the previous position and returns the encountered value.
- *
- * @private
- * @returns {Object}
- * @returns {Boolean} return.done True if iterator is done.
- * @returns {engine.view.TreeWalkerValue} return.value Information about taken step.
- */
- _previous() {
- let position = Position.createFromPosition( this.position );
- const previousPosition = this.position;
- const parent = position.parent;
- // We are at the beginning of the root.
- if ( parent.parent === null && position.offset === 0 ) {
- return { done: true };
- }
- // We reached the walker boundary.
- if ( parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset ) {
- return { done: true };
- }
- // Get node just before current position.
- let node;
- // Text {@link engine.view.Text} element is a specific parent because contains string instead of child nodes.
- if ( parent instanceof Text ) {
- node = parent.data[ position.offset - 1 ];
- } else {
- node = parent.getChild( position.offset - 1 );
- }
- if ( node instanceof Element ) {
- if ( !this.shallow ) {
- position = new Position( node, node.childCount );
- this.position = position;
- if ( this.ignoreElementEnd ) {
- return this._previous();
- } else {
- return this._formatReturnValue( 'elementEnd', node, previousPosition, position );
- }
- } else {
- position.offset--;
- this.position = position;
- return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
- }
- } else if ( node instanceof Text ) {
- if ( this.singleCharacters ) {
- position = new Position( node, node.data.length );
- this.position = position;
- return this._previous();
- } else {
- let charactersCount = node.data.length;
- let item = node;
- // If text stick out of walker range, we need to cut it and wrap by TextProxy.
- if ( node == this._boundaryStartParent ) {
- const offset = this.boundaries.start.offset;
- item = new TextProxy( node, offset, node.data.length - offset );
- charactersCount = item.data.length;
- position = Position.createBefore( item );
- } else {
- // If not just keep moving backward.
- position.offset--;
- }
- this.position = position;
- return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount );
- }
- } else if ( typeof node == 'string' ) {
- let textLength;
- if ( !this.singleCharacters ) {
- // Check if text stick out of walker range.
- const startOffset = parent === this._boundaryStartParent ? this.boundaries.start.offset : 0;
- textLength = position.offset - startOffset;
- } else {
- textLength = 1;
- }
- position.offset -= textLength;
- const textProxy = new TextProxy( parent, position.offset, textLength );
- this.position = position;
- return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
- } else {
- // `node` is not set, we reached the beginning of current `parent`.
- position = Position.createBefore( parent );
- this.position = position;
- return this._formatReturnValue( 'elementStart', parent, previousPosition, position, 1 );
- }
- }
- /**
- * Format returned data and adjust `previousPosition` and `nextPosition` if reach the bound of the {@link engine.view.Text}.
- *
- * @private
- * @param {engine.view.TreeWalkerValueType} type Type of step.
- * @param {engine.view.Item} item Item between old and new position.
- * @param {engine.view.Position} previousPosition Previous position of iterator.
- * @param {engine.view.Position} nextPosition Next position of iterator.
- * @param {Number} [length] Length of the item.
- * @returns {engine.view.TreeWalkerValue}
- */
- _formatReturnValue( type, item, previousPosition, nextPosition, length ) {
- // Text is a specific parent, because contains string instead of childs.
- // Walker doesn't enter to the Text except situations when walker is iterating over every single character,
- // or the bound starts/ends inside the Text. So when the position is at the beginning or at the end of the Text
- // we move it just before or just after Text.
- if ( item instanceof TextProxy ) {
- // Position is at the end of Text.
- if ( item.offsetInText + item.data.length == item.textNode.data.length ) {
- if ( this.direction == 'forward' && !( this.boundaries && this.boundaries.end.isEqual( this.position ) ) ) {
- nextPosition = Position.createAfter( item.textNode );
- // When we change nextPosition of returned value we need also update walker current position.
- this.position = nextPosition;
- } else {
- previousPosition = Position.createAfter( item.textNode );
- }
- }
- // Position is at the begining ot the text.
- if ( item.offsetInText === 0 ) {
- if ( this.direction == 'backward' && !( this.boundaries && this.boundaries.start.isEqual( this.position ) ) ) {
- nextPosition = Position.createBefore( item.textNode );
- // When we change nextPosition of returned value we need also update walker current position.
- this.position = nextPosition;
- } else {
- previousPosition = Position.createBefore( item.textNode );
- }
- }
- }
- return {
- done: false,
- value: {
- type: type,
- item: item,
- previousPosition: previousPosition,
- nextPosition: nextPosition,
- length: length
- }
- };
- }
- }
- /**
- * Type of the step made by {@link engine.view.TreeWalker}.
- * Possible values: `'elementStart'` if walker is at the beginning of a node, `'elementEnd'` if walker is at the end
- * of node, or `'text'` if walker traversed over single and multiple characters.
- * For {@link engine.view.Text} `elementStart` and `elementEnd` is not returned.
- *
- * @typedef {String} engine.view.TreeWalkerValueType
- */
- /**
- * Object returned by {@link engine.view.TreeWalker} when traversing tree view.
- *
- * @typedef {Object} engine.view.TreeWalkerValue
- * @property {engine.view.TreeWalkerValueType} type
- * @property {engine.view.Item} item Item between old and new positions of {@link engine.view.TreeWalker}.
- * @property {engine.view.Position} previousPosition Previous position of the iterator.
- * * Forward iteration: For `'elementEnd'` it is the last position inside the element. For all other types it is the
- * position before the item. Note that it is more efficient to use this position then calculate the position before
- * the node using {@link engine.view.Position.createBefore}.
- * * Backward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is
- * the position after item.
- * * If the position is at the beginning or at the end of the {@link engine.view.Text} it is always moved from the
- * inside of the Text to its parent just before or just after Text.
- * @property {engine.view.Position} nextPosition Next position of the iterator.
- * * Forward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is
- * the position after the item.
- * * Backward iteration: For `'elementEnd'` it is last position inside element. For all other types it is the position
- * before the item.
- * * If the position is at the beginning or at the end of the {@link engine.view.Text} it is always moved from the
- * inside of the Text to its parent just before or just after Text.
- * @property {Number} [length] Length of the item. For `'elementStart'` it is 1. For `'text'` it is
- * the length of the text. For `'elementEnd'` it is undefined.
- */
- /**
- * Tree walking directions.
- *
- * @typedef {'forward'|'backward'} engine.view.TreeWalkerDirection
- */
|