treewalker.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import CharacterProxy from './characterproxy.js';
  7. import TextFragment from './textfragment.js';
  8. import Element from './element.js';
  9. import Position from './position.js';
  10. import CKEditorError from '../ckeditorerror.js';
  11. const ELEMENT_ENTER = 0;
  12. const ELEMENT_LEAVE = 1;
  13. const TEXT = 2;
  14. const CHARACTER = 3;
  15. /**
  16. * Position iterator class. It allows to iterate forward and backward over the tree document.
  17. *
  18. * @class treeModel.TreeWalker
  19. */
  20. export default class TreeWalker {
  21. /**
  22. * Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or `position`.
  23. *
  24. * @param {Object} options Object with configuration.
  25. * @param {treeModel.Range} [options.boundaries] Range to define boundaries of the iterator.
  26. * @param {treeModel.Position} [options.position] Starting position.
  27. * @param {Boolean} [options.mergeCharacters] Flag indicating whether all consecutive characters with the same attributes
  28. * should be returned as one {@link treeModel.TextFragment} (`true`) or one by one as multiple {@link treeModel.CharacterProxy}
  29. * (`false`) objects. Defaults to `false`.
  30. * @constructor
  31. */
  32. constructor( options ) {
  33. if ( !options || ( !options.boundaries && !options.position ) ) {
  34. /**
  35. * Neither boundaries nor starting position have been defined.
  36. *
  37. * @error tree-walker-no-start-position
  38. */
  39. throw new CKEditorError( 'tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
  40. }
  41. /**
  42. * Iterator boundaries.
  43. *
  44. * When the {@link #next} method is called on the end boundary or the {@link #previous} method
  45. * on the start boundary, then `{ done: true }` is returned.
  46. *
  47. * If boundaries are not defined they are set before first and after last child of the root node.
  48. *
  49. * @property {treeModel.Range} boundaries
  50. */
  51. this.boundaries = options.boundaries ? options.boundaries : null;
  52. /**
  53. * Iterator position.
  54. *
  55. * @property {treeModel.Position} position
  56. */
  57. this.position = options.position ? options.position : options.boundaries.start;
  58. /**
  59. * Flag indicating whether all consecutive characters with the same attributes should be
  60. * returned as one {@link treeModel.CharacterProxy} (`true`) or one by one (`false`).
  61. *
  62. * @property {Boolean} mergeCharacters
  63. */
  64. this.mergeCharacters = !!options.mergeCharacters;
  65. }
  66. /**
  67. * Moves the {@link #position} to the next position and returns the encountered value.
  68. *
  69. * @returns {Object} Value between the previous and the new {@link #position}.
  70. * @returns {Boolean} return.done True if iterator is done.
  71. * @returns {Object} return.value
  72. * @returns {Number} return.value.type Encountered value type, possible options: {@link TreeWalker#ELEMENT_ENTER},
  73. * {@link TreeWalker#ELEMENT_LEAVE} or {@link TreeWalker#TEXT}.
  74. * @returns {treeModel.Node} return.value.node Encountered node.
  75. */
  76. next() {
  77. const position = this.position;
  78. const parent = position.parent;
  79. // We are at the end of the root.
  80. if ( parent.parent === null && position.offset === parent.getChildCount() ) {
  81. return { done: true };
  82. }
  83. if ( this.boundaries && position.isEqual( this.boundaries.end ) ) {
  84. return { done: true };
  85. }
  86. const nodeAfter = position.nodeAfter;
  87. if ( nodeAfter instanceof Element ) {
  88. this.position = Position.createFromParentAndOffset( nodeAfter, 0 );
  89. return formatReturnValue( ELEMENT_ENTER, nodeAfter );
  90. } else if ( nodeAfter instanceof CharacterProxy ) {
  91. if ( this.mergeCharacters ) {
  92. let charactersCount = nodeAfter._nodeListText.text.length - nodeAfter._index;
  93. let offset = position.offset + charactersCount;
  94. if ( this.boundaries && this.boundaries.end.parent == parent && this.boundaries.end.offset < offset ) {
  95. offset = this.boundaries.end.offset;
  96. charactersCount = offset - position.offset;
  97. }
  98. let text = nodeAfter._nodeListText.text.substr( nodeAfter._index, charactersCount );
  99. let textFragment = new TextFragment( position, text );
  100. this.position = Position.createFromParentAndOffset( parent, offset );
  101. return formatReturnValue( TEXT, textFragment );
  102. } else {
  103. this.position = Position.createFromParentAndOffset( parent, position.offset + 1 );
  104. return formatReturnValue( CHARACTER, nodeAfter );
  105. }
  106. } else {
  107. this.position = Position.createFromParentAndOffset( parent.parent, parent.getIndex() + 1 );
  108. return formatReturnValue( ELEMENT_LEAVE, this.position.nodeBefore );
  109. }
  110. }
  111. /**
  112. * Moves the {@link #position} to the previous position and returns the encountered value.
  113. *
  114. * @returns {Object} Value between the previous and the new {@link #position}.
  115. * @returns {Boolean} return.done True if iterator is done.
  116. * @returns {Object} return.value
  117. * @returns {Number} return.value.type Encountered value type, possible options: {@link TreeWalker#ELEMENT_ENTER},
  118. * {@link TreeWalker#ELEMENT_LEAVE} or {@link TreeWalker#TEXT}.
  119. * @returns {treeModel.Node} return.value.item Scanned node.
  120. */
  121. previous() {
  122. const position = this.position;
  123. const parent = position.parent;
  124. // We are at the beginning of the root.
  125. if ( parent.parent === null && position.offset === 0 ) {
  126. return { done: true };
  127. }
  128. if ( this.boundaries && position.isEqual( this.boundaries.start ) ) {
  129. return { done: true };
  130. }
  131. const nodeBefore = position.nodeBefore;
  132. if ( nodeBefore instanceof Element ) {
  133. this.position = Position.createFromParentAndOffset( nodeBefore, nodeBefore.getChildCount() );
  134. return formatReturnValue( ELEMENT_LEAVE, nodeBefore );
  135. } else if ( nodeBefore instanceof CharacterProxy ) {
  136. if ( this.mergeCharacters ) {
  137. let charactersCount = nodeBefore._index + 1;
  138. let offset = position.offset - charactersCount;
  139. if ( this.boundaries && this.boundaries.start.parent == parent && this.boundaries.start.offset > offset ) {
  140. offset = this.boundaries.start.offset;
  141. charactersCount = position.offset - offset;
  142. }
  143. let text = nodeBefore._nodeListText.text.substr( nodeBefore._index + 1 - charactersCount, charactersCount );
  144. this.position = Position.createFromParentAndOffset( parent, offset );
  145. let textFragment = new TextFragment( this.position, text );
  146. return formatReturnValue( TEXT, textFragment );
  147. } else {
  148. this.position = Position.createFromParentAndOffset( parent, position.offset - 1 );
  149. return formatReturnValue( CHARACTER, nodeBefore );
  150. }
  151. } else {
  152. this.position = Position.createFromParentAndOffset( parent.parent, parent.getIndex() );
  153. return formatReturnValue( ELEMENT_ENTER, this.position.nodeAfter );
  154. }
  155. }
  156. }
  157. function formatReturnValue( type, item ) {
  158. return {
  159. done: false,
  160. value: {
  161. type: type,
  162. item: item
  163. }
  164. };
  165. }
  166. /**
  167. * Flag for entering element.
  168. *
  169. * @static
  170. * @readonly
  171. * @property {Number}
  172. */
  173. TreeWalker.ELEMENT_ENTER = ELEMENT_ENTER;
  174. /**
  175. * Flag for leaving element.
  176. *
  177. * @static
  178. * @readonly
  179. * @property {Number}
  180. */
  181. TreeWalker.ELEMENT_LEAVE = ELEMENT_LEAVE;
  182. /**
  183. * Flag for text.
  184. *
  185. * @static
  186. * @readonly
  187. * @property {Number}
  188. */
  189. TreeWalker.TEXT = TEXT;
  190. /**
  191. * Flag for character.
  192. *
  193. * @static
  194. * @readonly
  195. * @property {Number}
  196. */
  197. TreeWalker.CHARACTER = CHARACTER;