1 /* Copyright (c) 2014 The Chromium Authors. All rights reserved.
2 * Use of this source code is governed by a BSD-style license that can be
3 * found in the LICENSE file. */
6 * @fileoverview Low-level DOM traversal utility functions to find the
7 * next (or previous) character, word, sentence, line, or paragraph,
8 * in a completely stateless manner without actually manipulating the
13 * A class to represent a cursor location in the document,
14 * like the start position or end position of a selection range.
16 * Later this may be extended to support "virtual text" for an object,
17 * like the ALT text for an image.
19 * Note: we cache the text of a particular node at the time we
20 * traverse into it. Later we should add support for dynamically
22 * @param {Node} node The DOM node.
23 * @param {number} index The index of the character within the node.
24 * @param {string} text The cached text contents of the node.
27 Cursor = function(node, index, text) {
34 * @return {Cursor} A new cursor pointing to the same location.
36 Cursor.prototype.clone = function() {
37 return new Cursor(this.node, this.index, this.text);
41 * Modify this cursor to point to the location that another cursor points to.
42 * @param {Cursor} otherCursor The cursor to copy from.
44 Cursor.prototype.copyFrom = function(otherCursor) {
45 this.node = otherCursor.node;
46 this.index = otherCursor.index;
47 this.text = otherCursor.text;
51 * Utility functions for stateless DOM traversal.
54 TraverseUtil = function() {};
57 * Gets the text representation of a node. This allows us to substitute
58 * alt text, names, or titles for html elements that provide them.
59 * @param {Node} node A DOM node.
60 * @return {string} A text string representation of the node.
62 TraverseUtil.getNodeText = function(node) {
63 if (node.constructor == Text) {
71 * Return true if a node should be treated as a leaf node, because
72 * its children are properties of the object that shouldn't be traversed.
74 * TODO(dmazzoni): replace this with a predicate that detects nodes with
75 * ARIA roles and other objects that have their own description.
76 * For now we just detect a couple of common cases.
78 * @param {Node} node A DOM node.
79 * @return {boolean} True if the node should be treated as a leaf node.
81 TraverseUtil.treatAsLeafNode = function(node) {
82 return node.childNodes.length == 0 ||
83 node.nodeName == 'SELECT' ||
84 node.nodeName == 'OBJECT';
88 * Return true only if a single character is whitespace.
89 * From https://developer.mozilla.org/en/Whitespace_in_the_DOM,
90 * whitespace is defined as one of the characters
96 * @param {string} c A string containing a single character.
97 * @return {boolean} True if the character is whitespace, otherwise false.
99 TraverseUtil.isWhitespace = function(c) {
100 return (c == ' ' || c == '\n' || c == '\r' || c == '\t');
104 * Set the selection to the range between the given start and end cursors.
105 * @param {Cursor} start The desired start of the selection.
106 * @param {Cursor} end The desired end of the selection.
107 * @return {Selection} the selection object.
109 TraverseUtil.setSelection = function(start, end) {
110 var sel = window.getSelection();
111 sel.removeAllRanges();
112 var range = document.createRange();
113 range.setStart(start.node, start.index);
114 range.setEnd(end.node, end.index);
121 * Use the computed CSS style to figure out if this DOM node is currently
123 * @param {Node} node A HTML DOM node.
124 * @return {boolean} Whether or not the html node is visible.
126 TraverseUtil.isVisible = function(node) {
129 var style = window.getComputedStyle(/** @type {Element} */(node), null);
130 return (!!style && style.display != 'none' && style.visibility != 'hidden');
134 * Use the class name to figure out if this DOM node should be traversed.
135 * @param {Node} node A HTML DOM node.
136 * @return {boolean} Whether or not the html node should be traversed.
138 TraverseUtil.isSkipped = function(node) {
139 if (node.constructor == Text)
140 node = node.parentElement;
141 if (node.className == 'CaretBrowsing_Caret' ||
142 node.className == 'CaretBrowsing_AnimateCaret') {
149 * Moves the cursor forwards until it has crossed exactly one character.
150 * @param {Cursor} cursor The cursor location where the search should start.
151 * On exit, the cursor will be immediately to the right of the
152 * character returned.
153 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
154 * initial and final cursor position will be pushed onto this array.
155 * @return {?string} The character found, or null if the bottom of the
156 * document has been reached.
158 TraverseUtil.forwardsChar = function(cursor, nodesCrossed) {
160 // Move down until we get to a leaf node.
161 var childNode = null;
162 if (!TraverseUtil.treatAsLeafNode(cursor.node)) {
163 for (var i = cursor.index; i < cursor.node.childNodes.length; i++) {
164 var node = cursor.node.childNodes[i];
165 if (TraverseUtil.isSkipped(node)) {
166 nodesCrossed.push(node);
169 if (TraverseUtil.isVisible(node)) {
176 cursor.node = childNode;
178 cursor.text = TraverseUtil.getNodeText(cursor.node);
179 if (cursor.node.constructor != Text) {
180 nodesCrossed.push(cursor.node);
185 // Return the next character from this leaf node.
186 if (cursor.index < cursor.text.length)
187 return cursor.text[cursor.index++];
189 // Move to the next sibling, going up the tree as necessary.
190 while (cursor.node != null) {
191 // Try to move to the next sibling.
192 var siblingNode = null;
193 for (var node = cursor.node.nextSibling;
195 node = node.nextSibling) {
196 if (TraverseUtil.isSkipped(node)) {
197 nodesCrossed.push(node);
200 if (TraverseUtil.isVisible(node)) {
206 cursor.node = siblingNode;
207 cursor.text = TraverseUtil.getNodeText(siblingNode);
210 if (cursor.node.constructor != Text) {
211 nodesCrossed.push(cursor.node);
217 // Otherwise, move to the parent.
218 if (cursor.node.parentNode &&
219 cursor.node.parentNode.constructor != HTMLBodyElement) {
220 cursor.node = cursor.node.parentNode;
231 * Moves the cursor backwards until it has crossed exactly one character.
232 * @param {Cursor} cursor The cursor location where the search should start.
233 * On exit, the cursor will be immediately to the left of the
234 * character returned.
235 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
236 * initial and final cursor position will be pushed onto this array.
237 * @return {?string} The previous character, or null if the top of the
238 * document has been reached.
240 TraverseUtil.backwardsChar = function(cursor, nodesCrossed) {
242 // Move down until we get to a leaf node.
243 var childNode = null;
244 if (!TraverseUtil.treatAsLeafNode(cursor.node)) {
245 for (var i = cursor.index - 1; i >= 0; i--) {
246 var node = cursor.node.childNodes[i];
247 if (TraverseUtil.isSkipped(node)) {
248 nodesCrossed.push(node);
251 if (TraverseUtil.isVisible(node)) {
258 cursor.node = childNode;
259 cursor.text = TraverseUtil.getNodeText(cursor.node);
260 if (cursor.text.length)
261 cursor.index = cursor.text.length;
263 cursor.index = cursor.node.childNodes.length;
264 if (cursor.node.constructor != Text)
265 nodesCrossed.push(cursor.node);
269 // Return the previous character from this leaf node.
270 if (cursor.text.length > 0 && cursor.index > 0) {
271 return cursor.text[--cursor.index];
274 // Move to the previous sibling, going up the tree as necessary.
276 // Try to move to the previous sibling.
277 var siblingNode = null;
278 for (var node = cursor.node.previousSibling;
280 node = node.previousSibling) {
281 if (TraverseUtil.isSkipped(node)) {
282 nodesCrossed.push(node);
285 if (TraverseUtil.isVisible(node)) {
291 cursor.node = siblingNode;
292 cursor.text = TraverseUtil.getNodeText(siblingNode);
293 if (cursor.text.length)
294 cursor.index = cursor.text.length;
296 cursor.index = cursor.node.childNodes.length;
297 if (cursor.node.constructor != Text)
298 nodesCrossed.push(cursor.node);
302 // Otherwise, move to the parent.
303 if (cursor.node.parentNode &&
304 cursor.node.parentNode.constructor != HTMLBodyElement) {
305 cursor.node = cursor.node.parentNode;
316 * Finds the next character, starting from endCursor. Upon exit, startCursor
317 * and endCursor will surround the next character. If skipWhitespace is
318 * true, will skip until a real character is found. Otherwise, it will
319 * attempt to select all of the whitespace between the initial position
320 * of endCursor and the next non-whitespace character.
321 * @param {Cursor} startCursor On exit, points to the position before
323 * @param {Cursor} endCursor The position to start searching for the next
324 * char. On exit, will point to the position past the char.
325 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
326 * initial and final cursor position will be pushed onto this array.
327 * @param {boolean} skipWhitespace If true, will keep scanning until a
328 * non-whitespace character is found.
329 * @return {?string} The next char, or null if the bottom of the
330 * document has been reached.
332 TraverseUtil.getNextChar = function(
333 startCursor, endCursor, nodesCrossed, skipWhitespace) {
335 // Save the starting position and get the first character.
336 startCursor.copyFrom(endCursor);
337 var c = TraverseUtil.forwardsChar(endCursor, nodesCrossed);
341 // Keep track of whether the first character was whitespace.
342 var initialWhitespace = TraverseUtil.isWhitespace(c);
344 // Keep scanning until we find a non-whitespace or non-skipped character.
345 while ((TraverseUtil.isWhitespace(c)) ||
346 (TraverseUtil.isSkipped(endCursor.node))) {
347 c = TraverseUtil.forwardsChar(endCursor, nodesCrossed);
351 if (skipWhitespace || !initialWhitespace) {
352 // If skipWhitepace is true, or if the first character we encountered
353 // was not whitespace, return that non-whitespace character.
354 startCursor.copyFrom(endCursor);
359 for (var i = 0; i < nodesCrossed.length; i++) {
360 if (TraverseUtil.isSkipped(nodesCrossed[i])) {
361 // We need to make sure that startCursor and endCursor aren't
362 // surrounding a skippable node.
364 startCursor.copyFrom(endCursor);
369 // Otherwise, return all of the whitespace before that last character.
376 * Finds the previous character, starting from startCursor. Upon exit,
377 * startCursor and endCursor will surround the previous character.
378 * If skipWhitespace is true, will skip until a real character is found.
379 * Otherwise, it will attempt to select all of the whitespace between
380 * the initial position of endCursor and the next non-whitespace character.
381 * @param {Cursor} startCursor The position to start searching for the
382 * char. On exit, will point to the position before the char.
383 * @param {Cursor} endCursor The position to start searching for the next
384 * char. On exit, will point to the position past the char.
385 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
386 * initial and final cursor position will be pushed onto this array.
387 * @param {boolean} skipWhitespace If true, will keep scanning until a
388 * non-whitespace character is found.
389 * @return {?string} The previous char, or null if the top of the
390 * document has been reached.
392 TraverseUtil.getPreviousChar = function(
393 startCursor, endCursor, nodesCrossed, skipWhitespace) {
395 // Save the starting position and get the first character.
396 endCursor.copyFrom(startCursor);
397 var c = TraverseUtil.backwardsChar(startCursor, nodesCrossed);
401 // Keep track of whether the first character was whitespace.
402 var initialWhitespace = TraverseUtil.isWhitespace(c);
404 // Keep scanning until we find a non-whitespace or non-skipped character.
405 while ((TraverseUtil.isWhitespace(c)) ||
406 (TraverseUtil.isSkipped(startCursor.node))) {
407 c = TraverseUtil.backwardsChar(startCursor, nodesCrossed);
411 if (skipWhitespace || !initialWhitespace) {
412 // If skipWhitepace is true, or if the first character we encountered
413 // was not whitespace, return that non-whitespace character.
414 endCursor.copyFrom(startCursor);
418 for (var i = 0; i < nodesCrossed.length; i++) {
419 if (TraverseUtil.isSkipped(nodesCrossed[i])) {
421 endCursor.copyFrom(startCursor);
426 // Otherwise, return all of the whitespace before that last character.
433 * Finds the next word, starting from endCursor. Upon exit, startCursor
434 * and endCursor will surround the next word. A word is defined to be
435 * a string of 1 or more non-whitespace characters in the same DOM node.
436 * @param {Cursor} startCursor On exit, will point to the beginning of the
438 * @param {Cursor} endCursor The position to start searching for the next
439 * word. On exit, will point to the end of the word returned.
440 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
441 * initial and final cursor position will be pushed onto this array.
442 * @return {?string} The next word, or null if the bottom of the
443 * document has been reached.
445 TraverseUtil.getNextWord = function(startCursor, endCursor,
448 // Find the first non-whitespace or non-skipped character.
449 var cursor = endCursor.clone();
450 var c = TraverseUtil.forwardsChar(cursor, nodesCrossed);
453 while ((TraverseUtil.isWhitespace(c)) ||
454 (TraverseUtil.isSkipped(cursor.node))) {
455 c = TraverseUtil.forwardsChar(cursor, nodesCrossed);
460 // Set startCursor to the position immediately before the first
461 // character in our word. It's safe to decrement |index| because
462 // forwardsChar guarantees that the cursor will be immediately to the
463 // right of the returned character on exit.
464 startCursor.copyFrom(cursor);
467 // Keep building up our word until we reach a whitespace character or
468 // would cross a tag. Don't actually return any tags crossed, because this
469 // word goes up until the tag boundary but not past it.
470 endCursor.copyFrom(cursor);
472 var newNodesCrossed = [];
473 c = TraverseUtil.forwardsChar(cursor, newNodesCrossed);
477 while (!TraverseUtil.isWhitespace(c) &&
478 newNodesCrossed.length == 0) {
480 endCursor.copyFrom(cursor);
481 c = TraverseUtil.forwardsChar(cursor, newNodesCrossed);
490 * Finds the previous word, starting from startCursor. Upon exit, startCursor
491 * and endCursor will surround the previous word. A word is defined to be
492 * a string of 1 or more non-whitespace characters in the same DOM node.
493 * @param {Cursor} startCursor The position to start searching for the
494 * previous word. On exit, will point to the beginning of the
496 * @param {Cursor} endCursor On exit, will point to the end of the
498 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
499 * initial and final cursor position will be pushed onto this array.
500 * @return {?string} The previous word, or null if the bottom of the
501 * document has been reached.
503 TraverseUtil.getPreviousWord = function(startCursor, endCursor,
505 // Find the first non-whitespace or non-skipped character.
506 var cursor = startCursor.clone();
507 var c = TraverseUtil.backwardsChar(cursor, nodesCrossed);
510 while ((TraverseUtil.isWhitespace(c) ||
511 (TraverseUtil.isSkipped(cursor.node)))) {
512 c = TraverseUtil.backwardsChar(cursor, nodesCrossed);
517 // Set endCursor to the position immediately after the first
518 // character we've found (the last character of the word, since we're
519 // searching backwards).
520 endCursor.copyFrom(cursor);
523 // Keep building up our word until we reach a whitespace character or
524 // would cross a tag. Don't actually return any tags crossed, because this
525 // word goes up until the tag boundary but not past it.
526 startCursor.copyFrom(cursor);
528 var newNodesCrossed = [];
529 c = TraverseUtil.backwardsChar(cursor, newNodesCrossed);
532 while (!TraverseUtil.isWhitespace(c) &&
533 newNodesCrossed.length == 0) {
535 startCursor.copyFrom(cursor);
536 c = TraverseUtil.backwardsChar(cursor, newNodesCrossed);
545 * Finds the next sentence, starting from endCursor. Upon exit,
546 * startCursor and endCursor will surround the next sentence.
548 * @param {Cursor} startCursor On exit, marks the beginning of the sentence.
549 * @param {Cursor} endCursor The position to start searching for the next
550 * sentence. On exit, will point to the end of the returned string.
551 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
552 * initial and final cursor position will be pushed onto this array.
553 * @param {Object} breakTags Associative array of tags that should break
555 * @return {?string} The next sentence, or null if the bottom of the
556 * document has been reached.
558 TraverseUtil.getNextSentence = function(
559 startCursor, endCursor, nodesCrossed, breakTags) {
560 return TraverseUtil.getNextString(
561 startCursor, endCursor, nodesCrossed,
562 function(str, word, nodes) {
563 if (str.substr(-1) == '.')
565 for (var i = 0; i < nodes.length; i++) {
566 if (TraverseUtil.isSkipped(nodes[i])) {
569 var style = window.getComputedStyle(nodes[i], null);
570 if (style && (style.display != 'inline' ||
571 breakTags[nodes[i].tagName])) {
580 * Finds the previous sentence, starting from startCursor. Upon exit,
581 * startCursor and endCursor will surround the previous sentence.
583 * @param {Cursor} startCursor The position to start searching for the next
584 * sentence. On exit, will point to the start of the returned string.
585 * @param {Cursor} endCursor On exit, the end of the returned string.
586 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
587 * initial and final cursor position will be pushed onto this array.
588 * @param {Object} breakTags Associative array of tags that should break
590 * @return {?string} The previous sentence, or null if the bottom of the
591 * document has been reached.
593 TraverseUtil.getPreviousSentence = function(
594 startCursor, endCursor, nodesCrossed, breakTags) {
595 return TraverseUtil.getPreviousString(
596 startCursor, endCursor, nodesCrossed,
597 function(str, word, nodes) {
598 if (word.substr(-1) == '.')
600 for (var i = 0; i < nodes.length; i++) {
601 if (TraverseUtil.isSkipped(nodes[i])) {
604 var style = window.getComputedStyle(nodes[i], null);
605 if (style && (style.display != 'inline' ||
606 breakTags[nodes[i].tagName])) {
615 * Finds the next line, starting from endCursor. Upon exit,
616 * startCursor and endCursor will surround the next line.
618 * @param {Cursor} startCursor On exit, marks the beginning of the line.
619 * @param {Cursor} endCursor The position to start searching for the next
620 * line. On exit, will point to the end of the returned string.
621 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
622 * initial and final cursor position will be pushed onto this array.
623 * @param {number} lineLength The maximum number of characters in a line.
624 * @param {Object} breakTags Associative array of tags that should break
626 * @return {?string} The next line, or null if the bottom of the
627 * document has been reached.
629 TraverseUtil.getNextLine = function(
630 startCursor, endCursor, nodesCrossed, lineLength, breakTags) {
631 return TraverseUtil.getNextString(
632 startCursor, endCursor, nodesCrossed,
633 function(str, word, nodes) {
634 if (str.length + word.length + 1 > lineLength)
636 for (var i = 0; i < nodes.length; i++) {
637 if (TraverseUtil.isSkipped(nodes[i])) {
640 var style = window.getComputedStyle(nodes[i], null);
641 if (style && (style.display != 'inline' ||
642 breakTags[nodes[i].tagName])) {
651 * Finds the previous line, starting from startCursor. Upon exit,
652 * startCursor and endCursor will surround the previous line.
654 * @param {Cursor} startCursor The position to start searching for the next
655 * line. On exit, will point to the start of the returned string.
656 * @param {Cursor} endCursor On exit, the end of the returned string.
657 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
658 * initial and final cursor position will be pushed onto this array.
659 * @param {number} lineLength The maximum number of characters in a line.
660 * @param {Object} breakTags Associative array of tags that should break
662 * @return {?string} The previous line, or null if the bottom of the
663 * document has been reached.
665 TraverseUtil.getPreviousLine = function(
666 startCursor, endCursor, nodesCrossed, lineLength, breakTags) {
667 return TraverseUtil.getPreviousString(
668 startCursor, endCursor, nodesCrossed,
669 function(str, word, nodes) {
670 if (str.length + word.length + 1 > lineLength)
672 for (var i = 0; i < nodes.length; i++) {
673 if (TraverseUtil.isSkipped(nodes[i])) {
676 var style = window.getComputedStyle(nodes[i], null);
677 if (style && (style.display != 'inline' ||
678 breakTags[nodes[i].tagName])) {
687 * Finds the next paragraph, starting from endCursor. Upon exit,
688 * startCursor and endCursor will surround the next paragraph.
690 * @param {Cursor} startCursor On exit, marks the beginning of the paragraph.
691 * @param {Cursor} endCursor The position to start searching for the next
692 * paragraph. On exit, will point to the end of the returned string.
693 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
694 * initial and final cursor position will be pushed onto this array.
695 * @return {?string} The next paragraph, or null if the bottom of the
696 * document has been reached.
698 TraverseUtil.getNextParagraph = function(startCursor, endCursor,
700 return TraverseUtil.getNextString(
701 startCursor, endCursor, nodesCrossed,
702 function(str, word, nodes) {
703 for (var i = 0; i < nodes.length; i++) {
704 if (TraverseUtil.isSkipped(nodes[i])) {
707 var style = window.getComputedStyle(nodes[i], null);
708 if (style && style.display != 'inline') {
717 * Finds the previous paragraph, starting from startCursor. Upon exit,
718 * startCursor and endCursor will surround the previous paragraph.
720 * @param {Cursor} startCursor The position to start searching for the next
721 * paragraph. On exit, will point to the start of the returned string.
722 * @param {Cursor} endCursor On exit, the end of the returned string.
723 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
724 * initial and final cursor position will be pushed onto this array.
725 * @return {?string} The previous paragraph, or null if the bottom of the
726 * document has been reached.
728 TraverseUtil.getPreviousParagraph = function(
729 startCursor, endCursor, nodesCrossed) {
730 return TraverseUtil.getPreviousString(
731 startCursor, endCursor, nodesCrossed,
732 function(str, word, nodes) {
733 for (var i = 0; i < nodes.length; i++) {
734 if (TraverseUtil.isSkipped(nodes[i])) {
737 var style = window.getComputedStyle(nodes[i], null);
738 if (style && style.display != 'inline') {
747 * Customizable function to return the next string of words in the DOM, based
748 * on provided functions to decide when to break one string and start
749 * the next. This can be used to get the next sentence, line, paragraph,
750 * or potentially other granularities.
752 * Finds the next contiguous string, starting from endCursor. Upon exit,
753 * startCursor and endCursor will surround the next string.
755 * The breakBefore function takes three parameters, and
756 * should return true if the string should be broken before the proposed
758 * str The string so far.
759 * word The next word to be added.
760 * nodesCrossed The nodes crossed in reaching this next word.
762 * @param {Cursor} startCursor On exit, will point to the beginning of the
764 * @param {Cursor} endCursor The position to start searching for the next
765 * string. On exit, will point to the end of the returned string.
766 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
767 * initial and final cursor position will be pushed onto this array.
768 * @param {function(string, string, Array<string>)} breakBefore
769 * Function that takes the string so far, next word to be added, and
770 * nodes crossed, and returns true if the string should be ended before
772 * @return {?string} The next string, or null if the bottom of the
773 * document has been reached.
775 TraverseUtil.getNextString = function(
776 startCursor, endCursor, nodesCrossed, breakBefore) {
777 // Get the first word and set the start cursor to the start of the
779 var wordStartCursor = endCursor.clone();
780 var wordEndCursor = endCursor.clone();
781 var newNodesCrossed = [];
783 var word = TraverseUtil.getNextWord(
784 wordStartCursor, wordEndCursor, newNodesCrossed);
787 startCursor.copyFrom(wordStartCursor);
789 // Always add the first word when the string is empty, and then keep
790 // adding more words as long as breakBefore returns false
791 while (!str || !breakBefore(str, word, newNodesCrossed)) {
792 // Append this word, set the end cursor to the end of this word, and
793 // update the returned list of nodes crossed to include ones we crossed
794 // in reaching this word.
798 nodesCrossed = nodesCrossed.concat(newNodesCrossed);
799 endCursor.copyFrom(wordEndCursor);
801 // Get the next word and go back to the top of the loop.
802 newNodesCrossed = [];
803 word = TraverseUtil.getNextWord(
804 wordStartCursor, wordEndCursor, newNodesCrossed);
813 * Customizable function to return the previous string of words in the DOM,
814 * based on provided functions to decide when to break one string and start
815 * the next. See getNextString, above, for more details.
817 * Finds the previous contiguous string, starting from startCursor. Upon exit,
818 * startCursor and endCursor will surround the next string.
820 * @param {Cursor} startCursor The position to start searching for the
821 * previous string. On exit, will point to the beginning of the
823 * @param {Cursor} endCursor On exit, will point to the end of the
825 * @param {Array<Node>} nodesCrossed Any HTML nodes crossed between the
826 * initial and final cursor position will be pushed onto this array.
827 * @param {function(string, string, Array<string>)} breakBefore
828 * Function that takes the string so far, the word to be added, and
829 * nodes crossed, and returns true if the string should be ended before
831 * @return {?string} The next string, or null if the top of the
832 * document has been reached.
834 TraverseUtil.getPreviousString = function(
835 startCursor, endCursor, nodesCrossed, breakBefore) {
836 // Get the first word and set the end cursor to the end of the
838 var wordStartCursor = startCursor.clone();
839 var wordEndCursor = startCursor.clone();
840 var newNodesCrossed = [];
842 var word = TraverseUtil.getPreviousWord(
843 wordStartCursor, wordEndCursor, newNodesCrossed);
846 endCursor.copyFrom(wordEndCursor);
848 // Always add the first word when the string is empty, and then keep
849 // adding more words as long as breakBefore returns false
850 while (!str || !breakBefore(str, word, newNodesCrossed)) {
851 // Prepend this word, set the start cursor to the start of this word, and
852 // update the returned list of nodes crossed to include ones we crossed
853 // in reaching this word.
857 nodesCrossed = nodesCrossed.concat(newNodesCrossed);
858 startCursor.copyFrom(wordStartCursor);
860 // Get the previous word and go back to the top of the loop.
861 newNodesCrossed = [];
862 word = TraverseUtil.getPreviousWord(
863 wordStartCursor, wordEndCursor, newNodesCrossed);