Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / ui / accessibility / extensions / caretbrowsing / traverse_util.js
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. */
4
5 /**
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
9  *     selection.
10  */
11
12 /**
13  * A class to represent a cursor location in the document,
14  * like the start position or end position of a selection range.
15  *
16  * Later this may be extended to support "virtual text" for an object,
17  * like the ALT text for an image.
18  *
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
21  * reloading it.
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.
25  * @constructor
26  */
27 Cursor = function(node, index, text) {
28   this.node = node;
29   this.index = index;
30   this.text = text;
31 };
32
33 /**
34  * @return {Cursor} A new cursor pointing to the same location.
35  */
36 Cursor.prototype.clone = function() {
37   return new Cursor(this.node, this.index, this.text);
38 };
39
40 /**
41  * Modify this cursor to point to the location that another cursor points to.
42  * @param {Cursor} otherCursor The cursor to copy from.
43  */
44 Cursor.prototype.copyFrom = function(otherCursor) {
45   this.node = otherCursor.node;
46   this.index = otherCursor.index;
47   this.text = otherCursor.text;
48 };
49
50 /**
51  * Utility functions for stateless DOM traversal.
52  * @constructor
53  */
54 TraverseUtil = function() {};
55
56 /**
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.
61  */
62 TraverseUtil.getNodeText = function(node) {
63   if (node.constructor == Text) {
64     return node.data;
65   } else {
66     return '';
67   }
68 };
69
70 /**
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.
73  *
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.
77  *
78  * @param {Node} node A DOM node.
79  * @return {boolean} True if the node should be treated as a leaf node.
80  */
81 TraverseUtil.treatAsLeafNode = function(node) {
82   return node.childNodes.length == 0 ||
83          node.nodeName == 'SELECT' ||
84          node.nodeName == 'OBJECT';
85 };
86
87 /**
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
91  *  "\t" TAB \u0009
92  *  "\n" LF  \u000A
93  *  "\r" CR  \u000D
94  *  " "  SPC \u0020.
95  *
96  * @param {string} c A string containing a single character.
97  * @return {boolean} True if the character is whitespace, otherwise false.
98  */
99 TraverseUtil.isWhitespace = function(c) {
100   return (c == ' ' || c == '\n' || c == '\r' || c == '\t');
101 };
102
103 /**
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.
108  */
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);
115   sel.addRange(range);
116
117   return sel;
118 };
119
120 /**
121  * Use the computed CSS style to figure out if this DOM node is currently
122  * visible.
123  * @param {Node} node A HTML DOM node.
124  * @return {boolean} Whether or not the html node is visible.
125  */
126 TraverseUtil.isVisible = function(node) {
127   if (!node.style)
128     return true;
129   var style = window.getComputedStyle(/** @type {Element} */(node), null);
130   return (!!style && style.display != 'none' && style.visibility != 'hidden');
131 };
132
133 /**
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.
137  */
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') {
143     return true;
144   }
145   return false;
146 };
147
148 /**
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.
157  */
158 TraverseUtil.forwardsChar = function(cursor, nodesCrossed) {
159   while (true) {
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);
167           continue;
168         }
169         if (TraverseUtil.isVisible(node)) {
170           childNode = node;
171           break;
172         }
173       }
174     }
175     if (childNode) {
176       cursor.node = childNode;
177       cursor.index = 0;
178       cursor.text = TraverseUtil.getNodeText(cursor.node);
179       if (cursor.node.constructor != Text) {
180         nodesCrossed.push(cursor.node);
181       }
182       continue;
183     }
184
185     // Return the next character from this leaf node.
186     if (cursor.index < cursor.text.length)
187       return cursor.text[cursor.index++];
188
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;
194            node != null;
195            node = node.nextSibling) {
196         if (TraverseUtil.isSkipped(node)) {
197           nodesCrossed.push(node);
198           continue;
199         }
200         if (TraverseUtil.isVisible(node)) {
201           siblingNode = node;
202           break;
203         }
204       }
205       if (siblingNode) {
206         cursor.node = siblingNode;
207         cursor.text = TraverseUtil.getNodeText(siblingNode);
208         cursor.index = 0;
209
210         if (cursor.node.constructor != Text) {
211           nodesCrossed.push(cursor.node);
212         }
213
214         break;
215       }
216
217       // Otherwise, move to the parent.
218       if (cursor.node.parentNode &&
219           cursor.node.parentNode.constructor != HTMLBodyElement) {
220         cursor.node = cursor.node.parentNode;
221         cursor.text = null;
222         cursor.index = 0;
223       } else {
224         return null;
225       }
226     }
227   }
228 };
229
230 /**
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.
239  */
240 TraverseUtil.backwardsChar = function(cursor, nodesCrossed) {
241   while (true) {
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);
249           continue;
250         }
251         if (TraverseUtil.isVisible(node)) {
252           childNode = node;
253           break;
254         }
255       }
256     }
257     if (childNode) {
258       cursor.node = childNode;
259       cursor.text = TraverseUtil.getNodeText(cursor.node);
260       if (cursor.text.length)
261         cursor.index = cursor.text.length;
262       else
263         cursor.index = cursor.node.childNodes.length;
264       if (cursor.node.constructor != Text)
265         nodesCrossed.push(cursor.node);
266       continue;
267     }
268
269     // Return the previous character from this leaf node.
270     if (cursor.text.length > 0 && cursor.index > 0) {
271       return cursor.text[--cursor.index];
272     }
273
274     // Move to the previous sibling, going up the tree as necessary.
275     while (true) {
276       // Try to move to the previous sibling.
277       var siblingNode = null;
278       for (var node = cursor.node.previousSibling;
279            node != null;
280            node = node.previousSibling) {
281         if (TraverseUtil.isSkipped(node)) {
282           nodesCrossed.push(node);
283           continue;
284         }
285         if (TraverseUtil.isVisible(node)) {
286           siblingNode = node;
287           break;
288         }
289       }
290       if (siblingNode) {
291         cursor.node = siblingNode;
292         cursor.text = TraverseUtil.getNodeText(siblingNode);
293         if (cursor.text.length)
294           cursor.index = cursor.text.length;
295         else
296           cursor.index = cursor.node.childNodes.length;
297         if (cursor.node.constructor != Text)
298           nodesCrossed.push(cursor.node);
299         break;
300       }
301
302       // Otherwise, move to the parent.
303       if (cursor.node.parentNode &&
304           cursor.node.parentNode.constructor != HTMLBodyElement) {
305         cursor.node = cursor.node.parentNode;
306         cursor.text = null;
307         cursor.index = 0;
308       } else {
309         return null;
310       }
311     }
312   }
313 };
314
315 /**
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
322  *     the char.
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.
331  */
332 TraverseUtil.getNextChar = function(
333     startCursor, endCursor, nodesCrossed, skipWhitespace) {
334
335   // Save the starting position and get the first character.
336   startCursor.copyFrom(endCursor);
337   var c = TraverseUtil.forwardsChar(endCursor, nodesCrossed);
338   if (c == null)
339     return null;
340
341   // Keep track of whether the first character was whitespace.
342   var initialWhitespace = TraverseUtil.isWhitespace(c);
343
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);
348     if (c == null)
349       return null;
350   }
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);
355     startCursor.index--;
356     return c;
357   }
358   else {
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.
363         endCursor.index--;
364         startCursor.copyFrom(endCursor);
365         startCursor.index--;
366         return ' ';
367       }
368     }
369     // Otherwise, return all of the whitespace before that last character.
370     endCursor.index--;
371     return ' ';
372   }
373 };
374
375 /**
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.
391  */
392 TraverseUtil.getPreviousChar = function(
393     startCursor, endCursor, nodesCrossed, skipWhitespace) {
394
395   // Save the starting position and get the first character.
396   endCursor.copyFrom(startCursor);
397   var c = TraverseUtil.backwardsChar(startCursor, nodesCrossed);
398   if (c == null)
399     return null;
400
401   // Keep track of whether the first character was whitespace.
402   var initialWhitespace = TraverseUtil.isWhitespace(c);
403
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);
408     if (c == null)
409       return null;
410   }
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);
415     endCursor.index++;
416     return c;
417   } else {
418     for (var i = 0; i < nodesCrossed.length; i++) {
419       if (TraverseUtil.isSkipped(nodesCrossed[i])) {
420         startCursor.index++;
421         endCursor.copyFrom(startCursor);
422         endCursor.index++;
423         return ' ';
424       }
425     }
426     // Otherwise, return all of the whitespace before that last character.
427     startCursor.index++;
428     return ' ';
429   }
430 };
431
432 /**
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
437  *     word returned.
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.
444  */
445 TraverseUtil.getNextWord = function(startCursor, endCursor,
446     nodesCrossed) {
447
448   // Find the first non-whitespace or non-skipped character.
449   var cursor = endCursor.clone();
450   var c = TraverseUtil.forwardsChar(cursor, nodesCrossed);
451   if (c == null)
452     return null;
453   while ((TraverseUtil.isWhitespace(c)) ||
454       (TraverseUtil.isSkipped(cursor.node))) {
455     c = TraverseUtil.forwardsChar(cursor, nodesCrossed);
456     if (c == null)
457       return null;
458   }
459
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);
465   startCursor.index--;
466
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);
471   var word = c;
472   var newNodesCrossed = [];
473   c = TraverseUtil.forwardsChar(cursor, newNodesCrossed);
474   if (c == null) {
475     return word;
476   }
477   while (!TraverseUtil.isWhitespace(c) &&
478      newNodesCrossed.length == 0) {
479     word += c;
480     endCursor.copyFrom(cursor);
481     c = TraverseUtil.forwardsChar(cursor, newNodesCrossed);
482     if (c == null) {
483       return word;
484     }
485   }
486   return word;
487 };
488
489 /**
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
495  *     word returned.
496  * @param {Cursor} endCursor On exit, will point to the end of the
497  *     word returned.
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.
502  */
503 TraverseUtil.getPreviousWord = function(startCursor, endCursor,
504     nodesCrossed) {
505   // Find the first non-whitespace or non-skipped character.
506   var cursor = startCursor.clone();
507   var c = TraverseUtil.backwardsChar(cursor, nodesCrossed);
508   if (c == null)
509     return null;
510   while ((TraverseUtil.isWhitespace(c) ||
511       (TraverseUtil.isSkipped(cursor.node)))) {
512     c = TraverseUtil.backwardsChar(cursor, nodesCrossed);
513     if (c == null)
514       return null;
515   }
516
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);
521   endCursor.index++;
522
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);
527   var word = c;
528   var newNodesCrossed = [];
529   c = TraverseUtil.backwardsChar(cursor, newNodesCrossed);
530   if (c == null)
531     return word;
532   while (!TraverseUtil.isWhitespace(c) &&
533       newNodesCrossed.length == 0) {
534     word = c + word;
535     startCursor.copyFrom(cursor);
536     c = TraverseUtil.backwardsChar(cursor, newNodesCrossed);
537     if (c == null)
538       return word;
539   }
540
541   return word;
542 };
543
544 /**
545  * Finds the next sentence, starting from endCursor.  Upon exit,
546  * startCursor and endCursor will surround the next sentence.
547  *
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
554  *     the sentence.
555  * @return {?string} The next sentence, or null if the bottom of the
556  *     document has been reached.
557  */
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) == '.')
564           return true;
565         for (var i = 0; i < nodes.length; i++) {
566           if (TraverseUtil.isSkipped(nodes[i])) {
567             return true;
568           }
569           var style = window.getComputedStyle(nodes[i], null);
570           if (style && (style.display != 'inline' ||
571                         breakTags[nodes[i].tagName])) {
572             return true;
573           }
574         }
575         return false;
576       });
577 };
578
579 /**
580  * Finds the previous sentence, starting from startCursor.  Upon exit,
581  * startCursor and endCursor will surround the previous sentence.
582  *
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
589  *     the sentence.
590  * @return {?string} The previous sentence, or null if the bottom of the
591  *     document has been reached.
592  */
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) == '.')
599           return true;
600         for (var i = 0; i < nodes.length; i++) {
601           if (TraverseUtil.isSkipped(nodes[i])) {
602             return true;
603           }
604           var style = window.getComputedStyle(nodes[i], null);
605           if (style && (style.display != 'inline' ||
606                         breakTags[nodes[i].tagName])) {
607             return true;
608           }
609         }
610         return false;
611       });
612 };
613
614 /**
615  * Finds the next line, starting from endCursor.  Upon exit,
616  * startCursor and endCursor will surround the next line.
617  *
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
625  *     the line.
626  * @return {?string} The next line, or null if the bottom of the
627  *     document has been reached.
628  */
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)
635           return true;
636         for (var i = 0; i < nodes.length; i++) {
637           if (TraverseUtil.isSkipped(nodes[i])) {
638             return true;
639           }
640           var style = window.getComputedStyle(nodes[i], null);
641           if (style && (style.display != 'inline' ||
642                         breakTags[nodes[i].tagName])) {
643             return true;
644           }
645         }
646         return false;
647       });
648 };
649
650 /**
651  * Finds the previous line, starting from startCursor.  Upon exit,
652  * startCursor and endCursor will surround the previous line.
653  *
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
661  *     the sentence.
662  *  @return {?string} The previous line, or null if the bottom of the
663  *     document has been reached.
664  */
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)
671           return true;
672         for (var i = 0; i < nodes.length; i++) {
673           if (TraverseUtil.isSkipped(nodes[i])) {
674             return true;
675           }
676           var style = window.getComputedStyle(nodes[i], null);
677           if (style && (style.display != 'inline' ||
678                         breakTags[nodes[i].tagName])) {
679             return true;
680           }
681         }
682         return false;
683       });
684 };
685
686 /**
687  * Finds the next paragraph, starting from endCursor.  Upon exit,
688  * startCursor and endCursor will surround the next paragraph.
689  *
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.
697  */
698 TraverseUtil.getNextParagraph = function(startCursor, endCursor,
699     nodesCrossed) {
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])) {
705             return true;
706           }
707           var style = window.getComputedStyle(nodes[i], null);
708           if (style && style.display != 'inline') {
709             return true;
710           }
711         }
712         return false;
713       });
714 };
715
716 /**
717  * Finds the previous paragraph, starting from startCursor.  Upon exit,
718  * startCursor and endCursor will surround the previous paragraph.
719  *
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.
727  */
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])) {
735             return true;
736           }
737           var style = window.getComputedStyle(nodes[i], null);
738           if (style && style.display != 'inline') {
739             return true;
740           }
741         }
742         return false;
743       });
744 };
745
746 /**
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.
751  *
752  * Finds the next contiguous string, starting from endCursor.  Upon exit,
753  * startCursor and endCursor will surround the next string.
754  *
755  * The breakBefore function takes three parameters, and
756  * should return true if the string should be broken before the proposed
757  * next word:
758  *   str The string so far.
759  *   word The next word to be added.
760  *   nodesCrossed The nodes crossed in reaching this next word.
761  *
762  * @param {Cursor} startCursor On exit, will point to the beginning of the
763  *     next string.
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
771  *     adding this word.
772  * @return {?string} The next string, or null if the bottom of the
773  *     document has been reached.
774  */
775 TraverseUtil.getNextString = function(
776     startCursor, endCursor, nodesCrossed, breakBefore) {
777   // Get the first word and set the start cursor to the start of the
778   // first word.
779   var wordStartCursor = endCursor.clone();
780   var wordEndCursor = endCursor.clone();
781   var newNodesCrossed = [];
782   var str = '';
783   var word = TraverseUtil.getNextWord(
784       wordStartCursor, wordEndCursor, newNodesCrossed);
785   if (word == null)
786     return null;
787   startCursor.copyFrom(wordStartCursor);
788
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.
795     if (str)
796       str += ' ';
797     str += word;
798     nodesCrossed = nodesCrossed.concat(newNodesCrossed);
799     endCursor.copyFrom(wordEndCursor);
800
801     // Get the next word and go back to the top of the loop.
802     newNodesCrossed = [];
803     word = TraverseUtil.getNextWord(
804         wordStartCursor, wordEndCursor, newNodesCrossed);
805     if (word == null)
806       return str;
807   }
808
809   return str;
810 };
811
812 /**
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.
816  *
817  * Finds the previous contiguous string, starting from startCursor.  Upon exit,
818  * startCursor and endCursor will surround the next string.
819  *
820  * @param {Cursor} startCursor The position to start searching for the
821  *     previous string.  On exit, will point to the beginning of the
822  *     string returned.
823  * @param {Cursor} endCursor On exit, will point to the end of the
824  *     string returned.
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
830  *     adding this word.
831  * @return {?string} The next string, or null if the top of the
832  *     document has been reached.
833  */
834 TraverseUtil.getPreviousString = function(
835     startCursor, endCursor, nodesCrossed, breakBefore) {
836   // Get the first word and set the end cursor to the end of the
837   // first word.
838   var wordStartCursor = startCursor.clone();
839   var wordEndCursor = startCursor.clone();
840   var newNodesCrossed = [];
841   var str = '';
842   var word = TraverseUtil.getPreviousWord(
843       wordStartCursor, wordEndCursor, newNodesCrossed);
844   if (word == null)
845     return null;
846   endCursor.copyFrom(wordEndCursor);
847
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.
854     if (str)
855       str = ' ' + str;
856     str = word + str;
857     nodesCrossed = nodesCrossed.concat(newNodesCrossed);
858     startCursor.copyFrom(wordStartCursor);
859 v
860     // Get the previous word and go back to the top of the loop.
861     newNodesCrossed = [];
862     word = TraverseUtil.getPreviousWord(
863         wordStartCursor, wordEndCursor, newNodesCrossed);
864     if (word == null)
865       return str;
866   }
867
868   return str;
869 };