11320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/* Copyright (c) 2014 The Chromium Authors. All rights reserved.
21320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Use of this source code is governed by a BSD-style license that can be
31320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * found in the LICENSE file. */
41320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
51320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
61320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @fileoverview Low-level DOM traversal utility functions to find the
71320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     next (or previous) character, word, sentence, line, or paragraph,
81320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     in a completely stateless manner without actually manipulating the
91320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     selection.
101320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
111320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
121320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
131320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * A class to represent a cursor location in the document,
141320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * like the start position or end position of a selection range.
151320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
161320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Later this may be extended to support "virtual text" for an object,
171320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * like the ALT text for an image.
181320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
191320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Note: we cache the text of a particular node at the time we
201320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * traverse into it. Later we should add support for dynamically
211320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * reloading it.
221320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Node} node The DOM node.
231320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {number} index The index of the character within the node.
241320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {string} text The cached text contents of the node.
251320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @constructor
261320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
271320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciCursor = function(node, index, text) {
281320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  this.node = node;
291320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  this.index = index;
301320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  this.text = text;
311320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
321320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
331320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
341320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {Cursor} A new cursor pointing to the same location.
351320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
361320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciCursor.prototype.clone = function() {
371320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return new Cursor(this.node, this.index, this.text);
381320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
391320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
401320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
411320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Modify this cursor to point to the location that another cursor points to.
421320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} otherCursor The cursor to copy from.
431320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
441320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciCursor.prototype.copyFrom = function(otherCursor) {
451320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  this.node = otherCursor.node;
461320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  this.index = otherCursor.index;
471320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  this.text = otherCursor.text;
481320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
491320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
501320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
511320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Utility functions for stateless DOM traversal.
521320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @constructor
531320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
541320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil = function() {};
551320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
561320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
571320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Gets the text representation of a node. This allows us to substitute
581320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * alt text, names, or titles for html elements that provide them.
591320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Node} node A DOM node.
601320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {string} A text string representation of the node.
611320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
621320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getNodeText = function(node) {
631320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (node.constructor == Text) {
641320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return node.data;
651320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  } else {
661320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return '';
671320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
681320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
691320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
701320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
711320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Return true if a node should be treated as a leaf node, because
721320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * its children are properties of the object that shouldn't be traversed.
731320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
741320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * TODO(dmazzoni): replace this with a predicate that detects nodes with
751320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * ARIA roles and other objects that have their own description.
761320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * For now we just detect a couple of common cases.
771320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
781320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Node} node A DOM node.
791320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {boolean} True if the node should be treated as a leaf node.
801320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
811320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.treatAsLeafNode = function(node) {
821320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return node.childNodes.length == 0 ||
831320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci         node.nodeName == 'SELECT' ||
841320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci         node.nodeName == 'OBJECT';
851320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
861320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
871320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
881320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Return true only if a single character is whitespace.
891320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * From https://developer.mozilla.org/en/Whitespace_in_the_DOM,
901320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * whitespace is defined as one of the characters
911320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *  "\t" TAB \u0009
921320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *  "\n" LF  \u000A
931320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *  "\r" CR  \u000D
941320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *  " "  SPC \u0020.
951320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
961320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {string} c A string containing a single character.
971320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {boolean} True if the character is whitespace, otherwise false.
981320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
991320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.isWhitespace = function(c) {
1001320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return (c == ' ' || c == '\n' || c == '\r' || c == '\t');
1011320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
1021320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
1031320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
1041320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Set the selection to the range between the given start and end cursors.
1051320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} start The desired start of the selection.
1061320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} end The desired end of the selection.
1071320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {Selection} the selection object.
1081320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
1091320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.setSelection = function(start, end) {
1101320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var sel = window.getSelection();
1111320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  sel.removeAllRanges();
1121320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var range = document.createRange();
1131320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  range.setStart(start.node, start.index);
1141320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  range.setEnd(end.node, end.index);
1151320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  sel.addRange(range);
1161320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
1171320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return sel;
1181320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
1191320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
1201320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
1211320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Use the computed CSS style to figure out if this DOM node is currently
1221320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * visible.
1231320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Node} node A HTML DOM node.
1241320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {boolean} Whether or not the html node is visible.
1251320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
1261320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.isVisible = function(node) {
1271320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (!node.style)
1281320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return true;
1291320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var style = window.getComputedStyle(/** @type {Element} */(node), null);
1301320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return (!!style && style.display != 'none' && style.visibility != 'hidden');
1311320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
1321320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
1331320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
1341320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Use the class name to figure out if this DOM node should be traversed.
1351320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Node} node A HTML DOM node.
1361320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {boolean} Whether or not the html node should be traversed.
1371320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
1381320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.isSkipped = function(node) {
1391320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (node.constructor == Text)
1401320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    node = node.parentElement;
1411320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (node.className == 'CaretBrowsing_Caret' ||
1421320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      node.className == 'CaretBrowsing_AnimateCaret') {
1431320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return true;
1441320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
1451320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return false;
1461320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
1471320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
1481320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
1491320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Moves the cursor forwards until it has crossed exactly one character.
1501320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} cursor The cursor location where the search should start.
1511320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     On exit, the cursor will be immediately to the right of the
1521320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     character returned.
1531320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
1541320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
1551320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The character found, or null if the bottom of the
1561320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
1571320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
1581320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.forwardsChar = function(cursor, nodesCrossed) {
1591320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  while (true) {
1601320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Move down until we get to a leaf node.
1611320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    var childNode = null;
1621320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (!TraverseUtil.treatAsLeafNode(cursor.node)) {
1631320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      for (var i = cursor.index; i < cursor.node.childNodes.length; i++) {
1641320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        var node = cursor.node.childNodes[i];
1651320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (TraverseUtil.isSkipped(node)) {
1661320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          nodesCrossed.push(node);
1671320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          continue;
1681320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
1691320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (TraverseUtil.isVisible(node)) {
1701320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          childNode = node;
1711320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          break;
1721320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
1731320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      }
1741320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    }
1751320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (childNode) {
1761320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      cursor.node = childNode;
1771320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      cursor.index = 0;
1781320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      cursor.text = TraverseUtil.getNodeText(cursor.node);
1791320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      if (cursor.node.constructor != Text) {
1801320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        nodesCrossed.push(cursor.node);
1811320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      }
1821320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      continue;
1831320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    }
1841320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
1851320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Return the next character from this leaf node.
1861320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (cursor.index < cursor.text.length)
1871320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      return cursor.text[cursor.index++];
1881320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
1891320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Move to the next sibling, going up the tree as necessary.
1901320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    while (cursor.node != null) {
1911320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      // Try to move to the next sibling.
1921320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      var siblingNode = null;
1931320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      for (var node = cursor.node.nextSibling;
1941320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci           node != null;
1951320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci           node = node.nextSibling) {
1961320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (TraverseUtil.isSkipped(node)) {
1971320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          nodesCrossed.push(node);
1981320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          continue;
1991320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
2001320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (TraverseUtil.isVisible(node)) {
2011320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          siblingNode = node;
2021320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          break;
2031320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
2041320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      }
2051320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      if (siblingNode) {
2061320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.node = siblingNode;
2071320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.text = TraverseUtil.getNodeText(siblingNode);
2081320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.index = 0;
2091320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
2101320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (cursor.node.constructor != Text) {
2111320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          nodesCrossed.push(cursor.node);
2121320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
2131320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
2141320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        break;
2151320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      }
2161320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
2171320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      // Otherwise, move to the parent.
2181320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      if (cursor.node.parentNode &&
2191320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          cursor.node.parentNode.constructor != HTMLBodyElement) {
2201320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.node = cursor.node.parentNode;
2211320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.text = null;
2221320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.index = 0;
2231320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      } else {
2241320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        return null;
2251320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      }
2261320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    }
2271320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
2281320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
2291320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
2301320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
2311320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Moves the cursor backwards until it has crossed exactly one character.
2321320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} cursor The cursor location where the search should start.
2331320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     On exit, the cursor will be immediately to the left of the
2341320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     character returned.
2351320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
2361320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
2371320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The previous character, or null if the top of the
2381320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
2391320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
2401320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.backwardsChar = function(cursor, nodesCrossed) {
2411320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  while (true) {
2421320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Move down until we get to a leaf node.
2431320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    var childNode = null;
2441320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (!TraverseUtil.treatAsLeafNode(cursor.node)) {
2451320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      for (var i = cursor.index - 1; i >= 0; i--) {
2461320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        var node = cursor.node.childNodes[i];
2471320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (TraverseUtil.isSkipped(node)) {
2481320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          nodesCrossed.push(node);
2491320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          continue;
2501320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
2511320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (TraverseUtil.isVisible(node)) {
2521320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          childNode = node;
2531320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          break;
2541320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
2551320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      }
2561320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    }
2571320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (childNode) {
2581320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      cursor.node = childNode;
2591320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      cursor.text = TraverseUtil.getNodeText(cursor.node);
2601320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      if (cursor.text.length)
2611320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.index = cursor.text.length;
2621320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      else
2631320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.index = cursor.node.childNodes.length;
2641320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      if (cursor.node.constructor != Text)
2651320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        nodesCrossed.push(cursor.node);
2661320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      continue;
2671320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    }
2681320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
2691320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Return the previous character from this leaf node.
2701320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (cursor.text.length > 0 && cursor.index > 0) {
2711320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      return cursor.text[--cursor.index];
2721320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    }
2731320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
2741320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Move to the previous sibling, going up the tree as necessary.
2751320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    while (true) {
2761320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      // Try to move to the previous sibling.
2771320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      var siblingNode = null;
2781320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      for (var node = cursor.node.previousSibling;
2791320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci           node != null;
2801320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci           node = node.previousSibling) {
2811320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (TraverseUtil.isSkipped(node)) {
2821320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          nodesCrossed.push(node);
2831320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          continue;
2841320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
2851320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (TraverseUtil.isVisible(node)) {
2861320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          siblingNode = node;
2871320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          break;
2881320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
2891320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      }
2901320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      if (siblingNode) {
2911320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.node = siblingNode;
2921320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.text = TraverseUtil.getNodeText(siblingNode);
2931320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (cursor.text.length)
2941320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          cursor.index = cursor.text.length;
2951320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        else
2961320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          cursor.index = cursor.node.childNodes.length;
2971320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (cursor.node.constructor != Text)
2981320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          nodesCrossed.push(cursor.node);
2991320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        break;
3001320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      }
3011320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
3021320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      // Otherwise, move to the parent.
3031320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      if (cursor.node.parentNode &&
3041320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          cursor.node.parentNode.constructor != HTMLBodyElement) {
3051320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.node = cursor.node.parentNode;
3061320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.text = null;
3071320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        cursor.index = 0;
3081320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      } else {
3091320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        return null;
3101320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      }
3111320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    }
3121320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
3131320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
3141320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
3151320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
3161320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the next character, starting from endCursor.  Upon exit, startCursor
3171320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * and endCursor will surround the next character. If skipWhitespace is
3181320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * true, will skip until a real character is found. Otherwise, it will
3191320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * attempt to select all of the whitespace between the initial position
3201320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * of endCursor and the next non-whitespace character.
3211320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor On exit, points to the position before
3221320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     the char.
3231320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor The position to start searching for the next
3241320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     char.  On exit, will point to the position past the char.
3251320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
3261320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
3271320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {boolean} skipWhitespace If true, will keep scanning until a
3281320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     non-whitespace character is found.
3291320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The next char, or null if the bottom of the
3301320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
3311320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
3321320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getNextChar = function(
3331320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor, endCursor, nodesCrossed, skipWhitespace) {
3341320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
3351320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Save the starting position and get the first character.
3361320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  startCursor.copyFrom(endCursor);
3371320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var c = TraverseUtil.forwardsChar(endCursor, nodesCrossed);
3381320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (c == null)
3391320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return null;
3401320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
3411320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Keep track of whether the first character was whitespace.
3421320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var initialWhitespace = TraverseUtil.isWhitespace(c);
3431320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
3441320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Keep scanning until we find a non-whitespace or non-skipped character.
3451320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  while ((TraverseUtil.isWhitespace(c)) ||
3461320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      (TraverseUtil.isSkipped(endCursor.node))) {
3471320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    c = TraverseUtil.forwardsChar(endCursor, nodesCrossed);
3481320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (c == null)
3491320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      return null;
3501320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
3511320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (skipWhitespace || !initialWhitespace) {
3521320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // If skipWhitepace is true, or if the first character we encountered
3531320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // was not whitespace, return that non-whitespace character.
3541320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor.copyFrom(endCursor);
3551320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor.index--;
3561320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return c;
3571320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
3581320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  else {
3591320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    for (var i = 0; i < nodesCrossed.length; i++) {
3601320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      if (TraverseUtil.isSkipped(nodesCrossed[i])) {
3611320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        // We need to make sure that startCursor and endCursor aren't
3621320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        // surrounding a skippable node.
3631320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        endCursor.index--;
3641320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        startCursor.copyFrom(endCursor);
3651320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        startCursor.index--;
3661320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        return ' ';
3671320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      }
3681320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    }
3691320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Otherwise, return all of the whitespace before that last character.
3701320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    endCursor.index--;
3711320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return ' ';
3721320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
3731320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
3741320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
3751320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
3761320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the previous character, starting from startCursor.  Upon exit,
3771320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * startCursor and endCursor will surround the previous character.
3781320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * If skipWhitespace is true, will skip until a real character is found.
3791320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Otherwise, it will attempt to select all of the whitespace between
3801320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * the initial position of endCursor and the next non-whitespace character.
3811320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor The position to start searching for the
3821320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     char. On exit, will point to the position before the char.
3831320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor The position to start searching for the next
3841320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     char. On exit, will point to the position past the char.
3851320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
3861320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
3871320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {boolean} skipWhitespace If true, will keep scanning until a
3881320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     non-whitespace character is found.
3891320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The previous char, or null if the top of the
3901320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
3911320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
3921320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getPreviousChar = function(
3931320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor, endCursor, nodesCrossed, skipWhitespace) {
3941320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
3951320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Save the starting position and get the first character.
3961320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  endCursor.copyFrom(startCursor);
3971320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var c = TraverseUtil.backwardsChar(startCursor, nodesCrossed);
3981320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (c == null)
3991320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return null;
4001320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
4011320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Keep track of whether the first character was whitespace.
4021320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var initialWhitespace = TraverseUtil.isWhitespace(c);
4031320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
4041320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Keep scanning until we find a non-whitespace or non-skipped character.
4051320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  while ((TraverseUtil.isWhitespace(c)) ||
4061320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      (TraverseUtil.isSkipped(startCursor.node))) {
4071320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    c = TraverseUtil.backwardsChar(startCursor, nodesCrossed);
4081320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (c == null)
4091320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      return null;
4101320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
4111320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (skipWhitespace || !initialWhitespace) {
4121320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // If skipWhitepace is true, or if the first character we encountered
4131320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // was not whitespace, return that non-whitespace character.
4141320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    endCursor.copyFrom(startCursor);
4151320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    endCursor.index++;
4161320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return c;
4171320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  } else {
4181320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    for (var i = 0; i < nodesCrossed.length; i++) {
4191320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      if (TraverseUtil.isSkipped(nodesCrossed[i])) {
4201320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        startCursor.index++;
4211320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        endCursor.copyFrom(startCursor);
4221320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        endCursor.index++;
4231320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        return ' ';
4241320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      }
4251320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    }
4261320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Otherwise, return all of the whitespace before that last character.
4271320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor.index++;
4281320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return ' ';
4291320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
4301320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
4311320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
4321320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
4331320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the next word, starting from endCursor.  Upon exit, startCursor
4341320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * and endCursor will surround the next word.  A word is defined to be
4351320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * a string of 1 or more non-whitespace characters in the same DOM node.
4361320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor On exit, will point to the beginning of the
4371320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     word returned.
4381320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor The position to start searching for the next
4391320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     word.  On exit, will point to the end of the word returned.
4401320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
4411320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
4421320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The next word, or null if the bottom of the
4431320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
4441320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
4451320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getNextWord = function(startCursor, endCursor,
4461320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    nodesCrossed) {
4471320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
4481320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Find the first non-whitespace or non-skipped character.
4491320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var cursor = endCursor.clone();
4501320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var c = TraverseUtil.forwardsChar(cursor, nodesCrossed);
4511320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (c == null)
4521320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return null;
4531320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  while ((TraverseUtil.isWhitespace(c)) ||
4541320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      (TraverseUtil.isSkipped(cursor.node))) {
4551320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    c = TraverseUtil.forwardsChar(cursor, nodesCrossed);
4561320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (c == null)
4571320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      return null;
4581320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
4591320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
4601320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Set startCursor to the position immediately before the first
4611320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // character in our word. It's safe to decrement |index| because
4621320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // forwardsChar guarantees that the cursor will be immediately to the
4631320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // right of the returned character on exit.
4641320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  startCursor.copyFrom(cursor);
4651320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  startCursor.index--;
4661320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
4671320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Keep building up our word until we reach a whitespace character or
4681320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // would cross a tag.  Don't actually return any tags crossed, because this
4691320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // word goes up until the tag boundary but not past it.
4701320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  endCursor.copyFrom(cursor);
4711320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var word = c;
4721320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var newNodesCrossed = [];
4731320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  c = TraverseUtil.forwardsChar(cursor, newNodesCrossed);
4741320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (c == null) {
4751320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return word;
4761320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
4771320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  while (!TraverseUtil.isWhitespace(c) &&
4781320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci     newNodesCrossed.length == 0) {
4791320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    word += c;
4801320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    endCursor.copyFrom(cursor);
4811320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    c = TraverseUtil.forwardsChar(cursor, newNodesCrossed);
4821320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (c == null) {
4831320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      return word;
4841320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    }
4851320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
4861320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return word;
4871320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
4881320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
4891320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
4901320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the previous word, starting from startCursor.  Upon exit, startCursor
4911320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * and endCursor will surround the previous word.  A word is defined to be
4921320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * a string of 1 or more non-whitespace characters in the same DOM node.
4931320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor The position to start searching for the
4941320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     previous word.  On exit, will point to the beginning of the
4951320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     word returned.
4961320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor On exit, will point to the end of the
4971320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     word returned.
4981320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
4991320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
5001320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The previous word, or null if the bottom of the
5011320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
5021320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
5031320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getPreviousWord = function(startCursor, endCursor,
5041320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    nodesCrossed) {
5051320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Find the first non-whitespace or non-skipped character.
5061320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var cursor = startCursor.clone();
5071320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var c = TraverseUtil.backwardsChar(cursor, nodesCrossed);
5081320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (c == null)
5091320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return null;
5101320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  while ((TraverseUtil.isWhitespace(c) ||
5111320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      (TraverseUtil.isSkipped(cursor.node)))) {
5121320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    c = TraverseUtil.backwardsChar(cursor, nodesCrossed);
5131320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (c == null)
5141320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      return null;
5151320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
5161320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
5171320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Set endCursor to the position immediately after the first
5181320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // character we've found (the last character of the word, since we're
5191320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // searching backwards).
5201320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  endCursor.copyFrom(cursor);
5211320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  endCursor.index++;
5221320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
5231320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Keep building up our word until we reach a whitespace character or
5241320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // would cross a tag.  Don't actually return any tags crossed, because this
5251320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // word goes up until the tag boundary but not past it.
5261320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  startCursor.copyFrom(cursor);
5271320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var word = c;
5281320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var newNodesCrossed = [];
5291320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  c = TraverseUtil.backwardsChar(cursor, newNodesCrossed);
5301320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (c == null)
5311320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return word;
5321320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  while (!TraverseUtil.isWhitespace(c) &&
5331320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      newNodesCrossed.length == 0) {
5341320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    word = c + word;
5351320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor.copyFrom(cursor);
5361320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    c = TraverseUtil.backwardsChar(cursor, newNodesCrossed);
5371320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (c == null)
5381320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      return word;
5391320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
5401320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
5411320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return word;
5421320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
5431320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
5441320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
5451320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the next sentence, starting from endCursor.  Upon exit,
5461320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * startCursor and endCursor will surround the next sentence.
5471320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
5481320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor On exit, marks the beginning of the sentence.
5491320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor The position to start searching for the next
5501320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     sentence.  On exit, will point to the end of the returned string.
5511320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
5521320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
5531320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Object} breakTags Associative array of tags that should break
5541320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     the sentence.
5551320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The next sentence, or null if the bottom of the
5561320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
5571320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
5581320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getNextSentence = function(
5591320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor, endCursor, nodesCrossed, breakTags) {
5601320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return TraverseUtil.getNextString(
5611320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      startCursor, endCursor, nodesCrossed,
5621320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      function(str, word, nodes) {
5631320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (str.substr(-1) == '.')
5641320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          return true;
5651320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        for (var i = 0; i < nodes.length; i++) {
5661320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (TraverseUtil.isSkipped(nodes[i])) {
5671320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
5681320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
5691320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          var style = window.getComputedStyle(nodes[i], null);
5701320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (style && (style.display != 'inline' ||
5711320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                        breakTags[nodes[i].tagName])) {
5721320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
5731320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
5741320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
5751320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        return false;
5761320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      });
5771320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
5781320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
5791320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
5801320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the previous sentence, starting from startCursor.  Upon exit,
5811320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * startCursor and endCursor will surround the previous sentence.
5821320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
5831320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor The position to start searching for the next
5841320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     sentence.  On exit, will point to the start of the returned string.
5851320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor On exit, the end of the returned string.
5861320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
5871320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
5881320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Object} breakTags Associative array of tags that should break
5891320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     the sentence.
5901320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The previous sentence, or null if the bottom of the
5911320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
5921320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
5931320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getPreviousSentence = function(
5941320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor, endCursor, nodesCrossed, breakTags) {
5951320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return TraverseUtil.getPreviousString(
5961320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      startCursor, endCursor, nodesCrossed,
5971320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      function(str, word, nodes) {
5981320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (word.substr(-1) == '.')
5991320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          return true;
6001320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        for (var i = 0; i < nodes.length; i++) {
6011320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (TraverseUtil.isSkipped(nodes[i])) {
6021320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
6031320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
6041320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          var style = window.getComputedStyle(nodes[i], null);
6051320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (style && (style.display != 'inline' ||
6061320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                        breakTags[nodes[i].tagName])) {
6071320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
6081320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
6091320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
6101320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        return false;
6111320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      });
6121320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
6131320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
6141320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
6151320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the next line, starting from endCursor.  Upon exit,
6161320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * startCursor and endCursor will surround the next line.
6171320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
6181320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor On exit, marks the beginning of the line.
6191320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor The position to start searching for the next
6201320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     line.  On exit, will point to the end of the returned string.
6211320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
6221320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
6231320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {number} lineLength The maximum number of characters in a line.
6241320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Object} breakTags Associative array of tags that should break
6251320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     the line.
6261320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The next line, or null if the bottom of the
6271320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
6281320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
6291320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getNextLine = function(
6301320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor, endCursor, nodesCrossed, lineLength, breakTags) {
6311320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return TraverseUtil.getNextString(
6321320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      startCursor, endCursor, nodesCrossed,
6331320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      function(str, word, nodes) {
6341320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (str.length + word.length + 1 > lineLength)
6351320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          return true;
6361320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        for (var i = 0; i < nodes.length; i++) {
6371320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (TraverseUtil.isSkipped(nodes[i])) {
6381320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
6391320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
6401320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          var style = window.getComputedStyle(nodes[i], null);
6411320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (style && (style.display != 'inline' ||
6421320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                        breakTags[nodes[i].tagName])) {
6431320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
6441320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
6451320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
6461320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        return false;
6471320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      });
6481320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
6491320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
6501320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
6511320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the previous line, starting from startCursor.  Upon exit,
6521320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * startCursor and endCursor will surround the previous line.
6531320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
6541320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor The position to start searching for the next
6551320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     line.  On exit, will point to the start of the returned string.
6561320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor On exit, the end of the returned string.
6571320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
6581320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
6591320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {number} lineLength The maximum number of characters in a line.
6601320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Object} breakTags Associative array of tags that should break
6611320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     the sentence.
6621320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *  @return {?string} The previous line, or null if the bottom of the
6631320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
6641320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
6651320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getPreviousLine = function(
6661320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor, endCursor, nodesCrossed, lineLength, breakTags) {
6671320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return TraverseUtil.getPreviousString(
6681320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      startCursor, endCursor, nodesCrossed,
6691320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      function(str, word, nodes) {
6701320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if (str.length + word.length + 1 > lineLength)
6711320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          return true;
6721320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        for (var i = 0; i < nodes.length; i++) {
6731320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (TraverseUtil.isSkipped(nodes[i])) {
6741320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
6751320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
6761320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          var style = window.getComputedStyle(nodes[i], null);
6771320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (style && (style.display != 'inline' ||
6781320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                        breakTags[nodes[i].tagName])) {
6791320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
6801320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
6811320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
6821320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        return false;
6831320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      });
6841320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
6851320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
6861320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
6871320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the next paragraph, starting from endCursor.  Upon exit,
6881320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * startCursor and endCursor will surround the next paragraph.
6891320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
6901320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor On exit, marks the beginning of the paragraph.
6911320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor The position to start searching for the next
6921320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     paragraph.  On exit, will point to the end of the returned string.
6931320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
6941320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
6951320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The next paragraph, or null if the bottom of the
6961320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
6971320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
6981320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getNextParagraph = function(startCursor, endCursor,
6991320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    nodesCrossed) {
7001320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return TraverseUtil.getNextString(
7011320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      startCursor, endCursor, nodesCrossed,
7021320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      function(str, word, nodes) {
7031320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        for (var i = 0; i < nodes.length; i++) {
7041320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (TraverseUtil.isSkipped(nodes[i])) {
7051320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
7061320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
7071320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          var style = window.getComputedStyle(nodes[i], null);
7081320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (style && style.display != 'inline') {
7091320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
7101320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
7111320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
7121320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        return false;
7131320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      });
7141320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
7151320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
7161320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
7171320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the previous paragraph, starting from startCursor.  Upon exit,
7181320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * startCursor and endCursor will surround the previous paragraph.
7191320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
7201320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor The position to start searching for the next
7211320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     paragraph.  On exit, will point to the start of the returned string.
7221320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor On exit, the end of the returned string.
7231320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
7241320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
7251320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The previous paragraph, or null if the bottom of the
7261320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
7271320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
7281320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getPreviousParagraph = function(
7291320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor, endCursor, nodesCrossed) {
7301320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return TraverseUtil.getPreviousString(
7311320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      startCursor, endCursor, nodesCrossed,
7321320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      function(str, word, nodes) {
7331320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        for (var i = 0; i < nodes.length; i++) {
7341320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (TraverseUtil.isSkipped(nodes[i])) {
7351320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
7361320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
7371320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          var style = window.getComputedStyle(nodes[i], null);
7381320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          if (style && style.display != 'inline') {
7391320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci            return true;
7401320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          }
7411320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        }
7421320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        return false;
7431320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      });
7441320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
7451320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
7461320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
7471320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Customizable function to return the next string of words in the DOM, based
7481320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * on provided functions to decide when to break one string and start
7491320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * the next. This can be used to get the next sentence, line, paragraph,
7501320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * or potentially other granularities.
7511320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
7521320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the next contiguous string, starting from endCursor.  Upon exit,
7531320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * startCursor and endCursor will surround the next string.
7541320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
7551320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * The breakBefore function takes three parameters, and
7561320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * should return true if the string should be broken before the proposed
7571320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * next word:
7581320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *   str The string so far.
7591320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *   word The next word to be added.
7601320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *   nodesCrossed The nodes crossed in reaching this next word.
7611320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
7621320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor On exit, will point to the beginning of the
7631320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     next string.
7641320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor The position to start searching for the next
7651320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     string.  On exit, will point to the end of the returned string.
7661320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
7671320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
7681320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {function(string, string, Array.<string>)} breakBefore
7691320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     Function that takes the string so far, next word to be added, and
7701320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     nodes crossed, and returns true if the string should be ended before
7711320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     adding this word.
7721320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The next string, or null if the bottom of the
7731320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
7741320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
7751320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getNextString = function(
7761320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor, endCursor, nodesCrossed, breakBefore) {
7771320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Get the first word and set the start cursor to the start of the
7781320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // first word.
7791320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var wordStartCursor = endCursor.clone();
7801320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var wordEndCursor = endCursor.clone();
7811320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var newNodesCrossed = [];
7821320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var str = '';
7831320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var word = TraverseUtil.getNextWord(
7841320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      wordStartCursor, wordEndCursor, newNodesCrossed);
7851320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (word == null)
7861320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return null;
7871320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  startCursor.copyFrom(wordStartCursor);
7881320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
7891320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Always add the first word when the string is empty, and then keep
7901320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // adding more words as long as breakBefore returns false
7911320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  while (!str || !breakBefore(str, word, newNodesCrossed)) {
7921320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Append this word, set the end cursor to the end of this word, and
7931320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // update the returned list of nodes crossed to include ones we crossed
7941320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // in reaching this word.
7951320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (str)
7961320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      str += ' ';
7971320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    str += word;
7981320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    nodesCrossed = nodesCrossed.concat(newNodesCrossed);
7991320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    endCursor.copyFrom(wordEndCursor);
8001320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
8011320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Get the next word and go back to the top of the loop.
8021320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    newNodesCrossed = [];
8031320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    word = TraverseUtil.getNextWord(
8041320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        wordStartCursor, wordEndCursor, newNodesCrossed);
8051320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (word == null)
8061320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      return str;
8071320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
8081320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
8091320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return str;
8101320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
8111320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
8121320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci/**
8131320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Customizable function to return the previous string of words in the DOM,
8141320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * based on provided functions to decide when to break one string and start
8151320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * the next. See getNextString, above, for more details.
8161320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
8171320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * Finds the previous contiguous string, starting from startCursor.  Upon exit,
8181320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * startCursor and endCursor will surround the next string.
8191320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *
8201320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} startCursor The position to start searching for the
8211320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     previous string.  On exit, will point to the beginning of the
8221320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     string returned.
8231320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Cursor} endCursor On exit, will point to the end of the
8241320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     string returned.
8251320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the
8261320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     initial and final cursor position will be pushed onto this array.
8271320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @param {function(string, string, Array.<string>)} breakBefore
8281320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     Function that takes the string so far, the word to be added, and
8291320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     nodes crossed, and returns true if the string should be ended before
8301320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     adding this word.
8311320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci * @return {?string} The next string, or null if the top of the
8321320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci *     document has been reached.
8331320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci */
8341320f92c476a1ad9d19dba2a48c72b75566198e9Primiano TucciTraverseUtil.getPreviousString = function(
8351320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor, endCursor, nodesCrossed, breakBefore) {
8361320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Get the first word and set the end cursor to the end of the
8371320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // first word.
8381320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var wordStartCursor = startCursor.clone();
8391320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var wordEndCursor = startCursor.clone();
8401320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var newNodesCrossed = [];
8411320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var str = '';
8421320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  var word = TraverseUtil.getPreviousWord(
8431320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      wordStartCursor, wordEndCursor, newNodesCrossed);
8441320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  if (word == null)
8451320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    return null;
8461320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  endCursor.copyFrom(wordEndCursor);
8471320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
8481320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // Always add the first word when the string is empty, and then keep
8491320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  // adding more words as long as breakBefore returns false
8501320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  while (!str || !breakBefore(str, word, newNodesCrossed)) {
8511320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Prepend this word, set the start cursor to the start of this word, and
8521320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // update the returned list of nodes crossed to include ones we crossed
8531320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // in reaching this word.
8541320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (str)
8551320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      str = ' ' + str;
8561320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    str = word + str;
8571320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    nodesCrossed = nodesCrossed.concat(newNodesCrossed);
8581320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    startCursor.copyFrom(wordStartCursor);
8591320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucciv
8601320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    // Get the previous word and go back to the top of the loop.
8611320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    newNodesCrossed = [];
8621320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    word = TraverseUtil.getPreviousWord(
8631320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        wordStartCursor, wordEndCursor, newNodesCrossed);
8641320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if (word == null)
8651320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      return str;
8661320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  }
8671320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci
8681320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  return str;
8691320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci};
870