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 }; 870