OLD | NEW |
| (Empty) |
1 // Copyright 2014 Google Inc. | |
2 // | |
3 // Licensed under the Apache License, Version 2.0 (the "License"); | |
4 // you may not use this file except in compliance with the License. | |
5 // You may obtain a copy of the License at | |
6 // | |
7 // http://www.apache.org/licenses/LICENSE-2.0 | |
8 // | |
9 // Unless required by applicable law or agreed to in writing, software | |
10 // distributed under the License is distributed on an "AS IS" BASIS, | |
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 // See the License for the specific language governing permissions and | |
13 // limitations under the License. | |
14 | |
15 /** | |
16 * @fileoverview Low-level DOM traversal utility functions to find the | |
17 * next (or previous) character, word, sentence, line, or paragraph, | |
18 * in a completely stateless manner without actually manipulating the | |
19 * selection. | |
20 */ | |
21 | |
22 /** | |
23 * A class to represent a cursor location in the document, | |
24 * like the start position or end position of a selection range. | |
25 * | |
26 * Later this may be extended to support "virtual text" for an object, | |
27 * like the ALT text for an image. | |
28 * | |
29 * Note: we cache the text of a particular node at the time we | |
30 * traverse into it. Later we should add support for dynamically | |
31 * reloading it. | |
32 * @param {Node} node The DOM node. | |
33 * @param {number} index The index of the character within the node. | |
34 * @param {string} text The cached text contents of the node. | |
35 * @constructor | |
36 */ | |
37 Cursor = function(node, index, text) { | |
38 this.node = node; | |
39 this.index = index; | |
40 this.text = text; | |
41 }; | |
42 | |
43 /** | |
44 * @return {Cursor} A new cursor pointing to the same location. | |
45 */ | |
46 Cursor.prototype.clone = function() { | |
47 return new Cursor(this.node, this.index, this.text); | |
48 }; | |
49 | |
50 /** | |
51 * Modify this cursor to point to the location that another cursor points to. | |
52 * @param {Cursor} otherCursor The cursor to copy from. | |
53 */ | |
54 Cursor.prototype.copyFrom = function(otherCursor) { | |
55 this.node = otherCursor.node; | |
56 this.index = otherCursor.index; | |
57 this.text = otherCursor.text; | |
58 }; | |
59 | |
60 /** | |
61 * Utility functions for stateless DOM traversal. | |
62 * @constructor | |
63 */ | |
64 TraverseUtil = function() {}; | |
65 | |
66 /** | |
67 * Gets the text representation of a node. This allows us to substitute | |
68 * alt text, names, or titles for html elements that provide them. | |
69 * @param {Node} node A DOM node. | |
70 * @return {string} A text string representation of the node. | |
71 */ | |
72 TraverseUtil.getNodeText = function(node) { | |
73 if (node.constructor == Text) { | |
74 return node.data; | |
75 } else { | |
76 return ''; | |
77 } | |
78 }; | |
79 | |
80 /** | |
81 * Return true if a node should be treated as a leaf node, because | |
82 * its children are properties of the object that shouldn't be traversed. | |
83 * | |
84 * TODO(dmazzoni): replace this with a predicate that detects nodes with | |
85 * ARIA roles and other objects that have their own description. | |
86 * For now we just detect a couple of common cases. | |
87 * | |
88 * @param {Node} node A DOM node. | |
89 * @return {boolean} True if the node should be treated as a leaf node. | |
90 */ | |
91 TraverseUtil.treatAsLeafNode = function(node) { | |
92 return node.childNodes.length == 0 || | |
93 node.nodeName == 'SELECT' || | |
94 node.nodeName == 'OBJECT'; | |
95 }; | |
96 | |
97 /** | |
98 * Return true only if a single character is whitespace. | |
99 * From https://developer.mozilla.org/en/Whitespace_in_the_DOM, | |
100 * whitespace is defined as one of the characters | |
101 * "\t" TAB \u0009 | |
102 * "\n" LF \u000A | |
103 * "\r" CR \u000D | |
104 * " " SPC \u0020. | |
105 * | |
106 * @param {string} c A string containing a single character. | |
107 * @return {boolean} True if the character is whitespace, otherwise false. | |
108 */ | |
109 TraverseUtil.isWhitespace = function(c) { | |
110 return (c == ' ' || c == '\n' || c == '\r' || c == '\t'); | |
111 }; | |
112 | |
113 /** | |
114 * Set the selection to the range between the given start and end cursors. | |
115 * @param {Cursor} start The desired start of the selection. | |
116 * @param {Cursor} end The desired end of the selection. | |
117 * @return {Selection} the selection object. | |
118 */ | |
119 TraverseUtil.setSelection = function(start, end) { | |
120 var sel = window.getSelection(); | |
121 sel.removeAllRanges(); | |
122 var range = document.createRange(); | |
123 range.setStart(start.node, start.index); | |
124 range.setEnd(end.node, end.index); | |
125 sel.addRange(range); | |
126 | |
127 return sel; | |
128 }; | |
129 | |
130 /** | |
131 * Use the computed CSS style to figure out if this DOM node is currently | |
132 * visible. | |
133 * @param {Node} node A HTML DOM node. | |
134 * @return {boolean} Whether or not the html node is visible. | |
135 */ | |
136 TraverseUtil.isVisible = function(node) { | |
137 if (!node.style) | |
138 return true; | |
139 var style = window.getComputedStyle(/** @type {Element} */(node), null); | |
140 return (!!style && style.display != 'none' && style.visibility != 'hidden'); | |
141 }; | |
142 | |
143 /** | |
144 * Use the class name to figure out if this DOM node should be traversed. | |
145 * @param {Node} node A HTML DOM node. | |
146 * @return {boolean} Whether or not the html node should be traversed. | |
147 */ | |
148 TraverseUtil.isSkipped = function(node) { | |
149 if (node.constructor == Text) | |
150 node = node.parentElement; | |
151 if (node.className == 'CaretBrowsing_Caret' || | |
152 node.className == 'CaretBrowsing_AnimateCaret') { | |
153 return true; | |
154 } | |
155 return false; | |
156 }; | |
157 | |
158 /** | |
159 * Moves the cursor forwards until it has crossed exactly one character. | |
160 * @param {Cursor} cursor The cursor location where the search should start. | |
161 * On exit, the cursor will be immediately to the right of the | |
162 * character returned. | |
163 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
164 * initial and final cursor position will be pushed onto this array. | |
165 * @return {?string} The character found, or null if the bottom of the | |
166 * document has been reached. | |
167 */ | |
168 TraverseUtil.forwardsChar = function(cursor, nodesCrossed) { | |
169 while (true) { | |
170 // Move down until we get to a leaf node. | |
171 var childNode = null; | |
172 if (!TraverseUtil.treatAsLeafNode(cursor.node)) { | |
173 for (var i = cursor.index; i < cursor.node.childNodes.length; i++) { | |
174 var node = cursor.node.childNodes[i]; | |
175 if (TraverseUtil.isSkipped(node)) { | |
176 nodesCrossed.push(node); | |
177 continue; | |
178 } | |
179 if (TraverseUtil.isVisible(node)) { | |
180 childNode = node; | |
181 break; | |
182 } | |
183 } | |
184 } | |
185 if (childNode) { | |
186 cursor.node = childNode; | |
187 cursor.index = 0; | |
188 cursor.text = TraverseUtil.getNodeText(cursor.node); | |
189 if (cursor.node.constructor != Text) { | |
190 nodesCrossed.push(cursor.node); | |
191 } | |
192 continue; | |
193 } | |
194 | |
195 // Return the next character from this leaf node. | |
196 if (cursor.index < cursor.text.length) | |
197 return cursor.text[cursor.index++]; | |
198 | |
199 // Move to the next sibling, going up the tree as necessary. | |
200 while (cursor.node != null) { | |
201 // Try to move to the next sibling. | |
202 var siblingNode = null; | |
203 for (var node = cursor.node.nextSibling; | |
204 node != null; | |
205 node = node.nextSibling) { | |
206 if (TraverseUtil.isSkipped(node)) { | |
207 nodesCrossed.push(node); | |
208 continue; | |
209 } | |
210 if (TraverseUtil.isVisible(node)) { | |
211 siblingNode = node; | |
212 break; | |
213 } | |
214 } | |
215 if (siblingNode) { | |
216 cursor.node = siblingNode; | |
217 cursor.text = TraverseUtil.getNodeText(siblingNode); | |
218 cursor.index = 0; | |
219 | |
220 if (cursor.node.constructor != Text) { | |
221 nodesCrossed.push(cursor.node); | |
222 } | |
223 | |
224 break; | |
225 } | |
226 | |
227 // Otherwise, move to the parent. | |
228 if (cursor.node.parentNode && | |
229 cursor.node.parentNode.constructor != HTMLBodyElement) { | |
230 cursor.node = cursor.node.parentNode; | |
231 cursor.text = null; | |
232 cursor.index = 0; | |
233 } else { | |
234 return null; | |
235 } | |
236 } | |
237 } | |
238 }; | |
239 | |
240 /** | |
241 * Moves the cursor backwards until it has crossed exactly one character. | |
242 * @param {Cursor} cursor The cursor location where the search should start. | |
243 * On exit, the cursor will be immediately to the left of the | |
244 * character returned. | |
245 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
246 * initial and final cursor position will be pushed onto this array. | |
247 * @return {?string} The previous character, or null if the top of the | |
248 * document has been reached. | |
249 */ | |
250 TraverseUtil.backwardsChar = function(cursor, nodesCrossed) { | |
251 while (true) { | |
252 // Move down until we get to a leaf node. | |
253 var childNode = null; | |
254 if (!TraverseUtil.treatAsLeafNode(cursor.node)) { | |
255 for (var i = cursor.index - 1; i >= 0; i--) { | |
256 var node = cursor.node.childNodes[i]; | |
257 if (TraverseUtil.isSkipped(node)) { | |
258 nodesCrossed.push(node); | |
259 continue; | |
260 } | |
261 if (TraverseUtil.isVisible(node)) { | |
262 childNode = node; | |
263 break; | |
264 } | |
265 } | |
266 } | |
267 if (childNode) { | |
268 cursor.node = childNode; | |
269 cursor.text = TraverseUtil.getNodeText(cursor.node); | |
270 if (cursor.text.length) | |
271 cursor.index = cursor.text.length; | |
272 else | |
273 cursor.index = cursor.node.childNodes.length; | |
274 if (cursor.node.constructor != Text) | |
275 nodesCrossed.push(cursor.node); | |
276 continue; | |
277 } | |
278 | |
279 // Return the previous character from this leaf node. | |
280 if (cursor.text.length > 0 && cursor.index > 0) { | |
281 return cursor.text[--cursor.index]; | |
282 } | |
283 | |
284 // Move to the previous sibling, going up the tree as necessary. | |
285 while (true) { | |
286 // Try to move to the previous sibling. | |
287 var siblingNode = null; | |
288 for (var node = cursor.node.previousSibling; | |
289 node != null; | |
290 node = node.previousSibling) { | |
291 if (TraverseUtil.isSkipped(node)) { | |
292 nodesCrossed.push(node); | |
293 continue; | |
294 } | |
295 if (TraverseUtil.isVisible(node)) { | |
296 siblingNode = node; | |
297 break; | |
298 } | |
299 } | |
300 if (siblingNode) { | |
301 cursor.node = siblingNode; | |
302 cursor.text = TraverseUtil.getNodeText(siblingNode); | |
303 if (cursor.text.length) | |
304 cursor.index = cursor.text.length; | |
305 else | |
306 cursor.index = cursor.node.childNodes.length; | |
307 if (cursor.node.constructor != Text) | |
308 nodesCrossed.push(cursor.node); | |
309 break; | |
310 } | |
311 | |
312 // Otherwise, move to the parent. | |
313 if (cursor.node.parentNode && | |
314 cursor.node.parentNode.constructor != HTMLBodyElement) { | |
315 cursor.node = cursor.node.parentNode; | |
316 cursor.text = null; | |
317 cursor.index = 0; | |
318 } else { | |
319 return null; | |
320 } | |
321 } | |
322 } | |
323 }; | |
324 | |
325 /** | |
326 * Finds the next character, starting from endCursor. Upon exit, startCursor | |
327 * and endCursor will surround the next character. If skipWhitespace is | |
328 * true, will skip until a real character is found. Otherwise, it will | |
329 * attempt to select all of the whitespace between the initial position | |
330 * of endCursor and the next non-whitespace character. | |
331 * @param {Cursor} startCursor On exit, points to the position before | |
332 * the char. | |
333 * @param {Cursor} endCursor The position to start searching for the next | |
334 * char. On exit, will point to the position past the char. | |
335 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
336 * initial and final cursor position will be pushed onto this array. | |
337 * @param {boolean} skipWhitespace If true, will keep scanning until a | |
338 * non-whitespace character is found. | |
339 * @return {?string} The next char, or null if the bottom of the | |
340 * document has been reached. | |
341 */ | |
342 TraverseUtil.getNextChar = function( | |
343 startCursor, endCursor, nodesCrossed, skipWhitespace) { | |
344 | |
345 // Save the starting position and get the first character. | |
346 startCursor.copyFrom(endCursor); | |
347 var c = TraverseUtil.forwardsChar(endCursor, nodesCrossed); | |
348 if (c == null) | |
349 return null; | |
350 | |
351 // Keep track of whether the first character was whitespace. | |
352 var initialWhitespace = TraverseUtil.isWhitespace(c); | |
353 | |
354 // Keep scanning until we find a non-whitespace or non-skipped character. | |
355 while ((TraverseUtil.isWhitespace(c)) || | |
356 (TraverseUtil.isSkipped(endCursor.node))) { | |
357 c = TraverseUtil.forwardsChar(endCursor, nodesCrossed); | |
358 if (c == null) | |
359 return null; | |
360 } | |
361 if (skipWhitespace || !initialWhitespace) { | |
362 // If skipWhitepace is true, or if the first character we encountered | |
363 // was not whitespace, return that non-whitespace character. | |
364 startCursor.copyFrom(endCursor); | |
365 startCursor.index--; | |
366 return c; | |
367 } | |
368 else { | |
369 for (var i = 0; i < nodesCrossed.length; i++) { | |
370 if (TraverseUtil.isSkipped(nodesCrossed[i])) { | |
371 // We need to make sure that startCursor and endCursor aren't | |
372 // surrounding a skippable node. | |
373 endCursor.index--; | |
374 startCursor.copyFrom(endCursor); | |
375 startCursor.index--; | |
376 return ' '; | |
377 } | |
378 } | |
379 // Otherwise, return all of the whitespace before that last character. | |
380 endCursor.index--; | |
381 return ' '; | |
382 } | |
383 }; | |
384 | |
385 /** | |
386 * Finds the previous character, starting from startCursor. Upon exit, | |
387 * startCursor and endCursor will surround the previous character. | |
388 * If skipWhitespace is true, will skip until a real character is found. | |
389 * Otherwise, it will attempt to select all of the whitespace between | |
390 * the initial position of endCursor and the next non-whitespace character. | |
391 * @param {Cursor} startCursor The position to start searching for the | |
392 * char. On exit, will point to the position before the char. | |
393 * @param {Cursor} endCursor The position to start searching for the next | |
394 * char. On exit, will point to the position past the char. | |
395 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
396 * initial and final cursor position will be pushed onto this array. | |
397 * @param {boolean} skipWhitespace If true, will keep scanning until a | |
398 * non-whitespace character is found. | |
399 * @return {?string} The previous char, or null if the top of the | |
400 * document has been reached. | |
401 */ | |
402 TraverseUtil.getPreviousChar = function( | |
403 startCursor, endCursor, nodesCrossed, skipWhitespace) { | |
404 | |
405 // Save the starting position and get the first character. | |
406 endCursor.copyFrom(startCursor); | |
407 var c = TraverseUtil.backwardsChar(startCursor, nodesCrossed); | |
408 if (c == null) | |
409 return null; | |
410 | |
411 // Keep track of whether the first character was whitespace. | |
412 var initialWhitespace = TraverseUtil.isWhitespace(c); | |
413 | |
414 // Keep scanning until we find a non-whitespace or non-skipped character. | |
415 while ((TraverseUtil.isWhitespace(c)) || | |
416 (TraverseUtil.isSkipped(startCursor.node))) { | |
417 c = TraverseUtil.backwardsChar(startCursor, nodesCrossed); | |
418 if (c == null) | |
419 return null; | |
420 } | |
421 if (skipWhitespace || !initialWhitespace) { | |
422 // If skipWhitepace is true, or if the first character we encountered | |
423 // was not whitespace, return that non-whitespace character. | |
424 endCursor.copyFrom(startCursor); | |
425 endCursor.index++; | |
426 return c; | |
427 } else { | |
428 for (var i = 0; i < nodesCrossed.length; i++) { | |
429 if (TraverseUtil.isSkipped(nodesCrossed[i])) { | |
430 startCursor.index++; | |
431 endCursor.copyFrom(startCursor); | |
432 endCursor.index++; | |
433 return ' '; | |
434 } | |
435 } | |
436 // Otherwise, return all of the whitespace before that last character. | |
437 startCursor.index++; | |
438 return ' '; | |
439 } | |
440 }; | |
441 | |
442 /** | |
443 * Finds the next word, starting from endCursor. Upon exit, startCursor | |
444 * and endCursor will surround the next word. A word is defined to be | |
445 * a string of 1 or more non-whitespace characters in the same DOM node. | |
446 * @param {Cursor} startCursor On exit, will point to the beginning of the | |
447 * word returned. | |
448 * @param {Cursor} endCursor The position to start searching for the next | |
449 * word. On exit, will point to the end of the word returned. | |
450 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
451 * initial and final cursor position will be pushed onto this array. | |
452 * @return {?string} The next word, or null if the bottom of the | |
453 * document has been reached. | |
454 */ | |
455 TraverseUtil.getNextWord = function(startCursor, endCursor, | |
456 nodesCrossed) { | |
457 | |
458 // Find the first non-whitespace or non-skipped character. | |
459 var cursor = endCursor.clone(); | |
460 var c = TraverseUtil.forwardsChar(cursor, nodesCrossed); | |
461 if (c == null) | |
462 return null; | |
463 while ((TraverseUtil.isWhitespace(c)) || | |
464 (TraverseUtil.isSkipped(cursor.node))) { | |
465 c = TraverseUtil.forwardsChar(cursor, nodesCrossed); | |
466 if (c == null) | |
467 return null; | |
468 } | |
469 | |
470 // Set startCursor to the position immediately before the first | |
471 // character in our word. It's safe to decrement |index| because | |
472 // forwardsChar guarantees that the cursor will be immediately to the | |
473 // right of the returned character on exit. | |
474 startCursor.copyFrom(cursor); | |
475 startCursor.index--; | |
476 | |
477 // Keep building up our word until we reach a whitespace character or | |
478 // would cross a tag. Don't actually return any tags crossed, because this | |
479 // word goes up until the tag boundary but not past it. | |
480 endCursor.copyFrom(cursor); | |
481 var word = c; | |
482 var newNodesCrossed = []; | |
483 c = TraverseUtil.forwardsChar(cursor, newNodesCrossed); | |
484 if (c == null) { | |
485 return word; | |
486 } | |
487 while (!TraverseUtil.isWhitespace(c) && | |
488 newNodesCrossed.length == 0) { | |
489 word += c; | |
490 endCursor.copyFrom(cursor); | |
491 c = TraverseUtil.forwardsChar(cursor, newNodesCrossed); | |
492 if (c == null) { | |
493 return word; | |
494 } | |
495 } | |
496 return word; | |
497 }; | |
498 | |
499 /** | |
500 * Finds the previous word, starting from startCursor. Upon exit, startCursor | |
501 * and endCursor will surround the previous word. A word is defined to be | |
502 * a string of 1 or more non-whitespace characters in the same DOM node. | |
503 * @param {Cursor} startCursor The position to start searching for the | |
504 * previous word. On exit, will point to the beginning of the | |
505 * word returned. | |
506 * @param {Cursor} endCursor On exit, will point to the end of the | |
507 * word returned. | |
508 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
509 * initial and final cursor position will be pushed onto this array. | |
510 * @return {?string} The previous word, or null if the bottom of the | |
511 * document has been reached. | |
512 */ | |
513 TraverseUtil.getPreviousWord = function(startCursor, endCursor, | |
514 nodesCrossed) { | |
515 // Find the first non-whitespace or non-skipped character. | |
516 var cursor = startCursor.clone(); | |
517 var c = TraverseUtil.backwardsChar(cursor, nodesCrossed); | |
518 if (c == null) | |
519 return null; | |
520 while ((TraverseUtil.isWhitespace(c) || | |
521 (TraverseUtil.isSkipped(cursor.node)))) { | |
522 c = TraverseUtil.backwardsChar(cursor, nodesCrossed); | |
523 if (c == null) | |
524 return null; | |
525 } | |
526 | |
527 // Set endCursor to the position immediately after the first | |
528 // character we've found (the last character of the word, since we're | |
529 // searching backwards). | |
530 endCursor.copyFrom(cursor); | |
531 endCursor.index++; | |
532 | |
533 // Keep building up our word until we reach a whitespace character or | |
534 // would cross a tag. Don't actually return any tags crossed, because this | |
535 // word goes up until the tag boundary but not past it. | |
536 startCursor.copyFrom(cursor); | |
537 var word = c; | |
538 var newNodesCrossed = []; | |
539 c = TraverseUtil.backwardsChar(cursor, newNodesCrossed); | |
540 if (c == null) | |
541 return word; | |
542 while (!TraverseUtil.isWhitespace(c) && | |
543 newNodesCrossed.length == 0) { | |
544 word = c + word; | |
545 startCursor.copyFrom(cursor); | |
546 c = TraverseUtil.backwardsChar(cursor, newNodesCrossed); | |
547 if (c == null) | |
548 return word; | |
549 } | |
550 | |
551 return word; | |
552 }; | |
553 | |
554 /** | |
555 * Finds the next sentence, starting from endCursor. Upon exit, | |
556 * startCursor and endCursor will surround the next sentence. | |
557 * | |
558 * @param {Cursor} startCursor On exit, marks the beginning of the sentence. | |
559 * @param {Cursor} endCursor The position to start searching for the next | |
560 * sentence. On exit, will point to the end of the returned string. | |
561 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
562 * initial and final cursor position will be pushed onto this array. | |
563 * @param {Object} breakTags Associative array of tags that should break | |
564 * the sentence. | |
565 * @return {?string} The next sentence, or null if the bottom of the | |
566 * document has been reached. | |
567 */ | |
568 TraverseUtil.getNextSentence = function( | |
569 startCursor, endCursor, nodesCrossed, breakTags) { | |
570 return TraverseUtil.getNextString( | |
571 startCursor, endCursor, nodesCrossed, | |
572 function(str, word, nodes) { | |
573 if (str.substr(-1) == '.') | |
574 return true; | |
575 for (var i = 0; i < nodes.length; i++) { | |
576 if (TraverseUtil.isSkipped(nodes[i])) { | |
577 return true; | |
578 } | |
579 var style = window.getComputedStyle(nodes[i], null); | |
580 if (style && (style.display != 'inline' || | |
581 breakTags[nodes[i].tagName])) { | |
582 return true; | |
583 } | |
584 } | |
585 return false; | |
586 }); | |
587 }; | |
588 | |
589 /** | |
590 * Finds the previous sentence, starting from startCursor. Upon exit, | |
591 * startCursor and endCursor will surround the previous sentence. | |
592 * | |
593 * @param {Cursor} startCursor The position to start searching for the next | |
594 * sentence. On exit, will point to the start of the returned string. | |
595 * @param {Cursor} endCursor On exit, the end of the returned string. | |
596 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
597 * initial and final cursor position will be pushed onto this array. | |
598 * @param {Object} breakTags Associative array of tags that should break | |
599 * the sentence. | |
600 * @return {?string} The previous sentence, or null if the bottom of the | |
601 * document has been reached. | |
602 */ | |
603 TraverseUtil.getPreviousSentence = function( | |
604 startCursor, endCursor, nodesCrossed, breakTags) { | |
605 return TraverseUtil.getPreviousString( | |
606 startCursor, endCursor, nodesCrossed, | |
607 function(str, word, nodes) { | |
608 if (word.substr(-1) == '.') | |
609 return true; | |
610 for (var i = 0; i < nodes.length; i++) { | |
611 if (TraverseUtil.isSkipped(nodes[i])) { | |
612 return true; | |
613 } | |
614 var style = window.getComputedStyle(nodes[i], null); | |
615 if (style && (style.display != 'inline' || | |
616 breakTags[nodes[i].tagName])) { | |
617 return true; | |
618 } | |
619 } | |
620 return false; | |
621 }); | |
622 }; | |
623 | |
624 /** | |
625 * Finds the next line, starting from endCursor. Upon exit, | |
626 * startCursor and endCursor will surround the next line. | |
627 * | |
628 * @param {Cursor} startCursor On exit, marks the beginning of the line. | |
629 * @param {Cursor} endCursor The position to start searching for the next | |
630 * line. On exit, will point to the end of the returned string. | |
631 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
632 * initial and final cursor position will be pushed onto this array. | |
633 * @param {number} lineLength The maximum number of characters in a line. | |
634 * @param {Object} breakTags Associative array of tags that should break | |
635 * the line. | |
636 * @return {?string} The next line, or null if the bottom of the | |
637 * document has been reached. | |
638 */ | |
639 TraverseUtil.getNextLine = function( | |
640 startCursor, endCursor, nodesCrossed, lineLength, breakTags) { | |
641 return TraverseUtil.getNextString( | |
642 startCursor, endCursor, nodesCrossed, | |
643 function(str, word, nodes) { | |
644 if (str.length + word.length + 1 > lineLength) | |
645 return true; | |
646 for (var i = 0; i < nodes.length; i++) { | |
647 if (TraverseUtil.isSkipped(nodes[i])) { | |
648 return true; | |
649 } | |
650 var style = window.getComputedStyle(nodes[i], null); | |
651 if (style && (style.display != 'inline' || | |
652 breakTags[nodes[i].tagName])) { | |
653 return true; | |
654 } | |
655 } | |
656 return false; | |
657 }); | |
658 }; | |
659 | |
660 /** | |
661 * Finds the previous line, starting from startCursor. Upon exit, | |
662 * startCursor and endCursor will surround the previous line. | |
663 * | |
664 * @param {Cursor} startCursor The position to start searching for the next | |
665 * line. On exit, will point to the start of the returned string. | |
666 * @param {Cursor} endCursor On exit, the end of the returned string. | |
667 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
668 * initial and final cursor position will be pushed onto this array. | |
669 * @param {number} lineLength The maximum number of characters in a line. | |
670 * @param {Object} breakTags Associative array of tags that should break | |
671 * the sentence. | |
672 * @return {?string} The previous line, or null if the bottom of the | |
673 * document has been reached. | |
674 */ | |
675 TraverseUtil.getPreviousLine = function( | |
676 startCursor, endCursor, nodesCrossed, lineLength, breakTags) { | |
677 return TraverseUtil.getPreviousString( | |
678 startCursor, endCursor, nodesCrossed, | |
679 function(str, word, nodes) { | |
680 if (str.length + word.length + 1 > lineLength) | |
681 return true; | |
682 for (var i = 0; i < nodes.length; i++) { | |
683 if (TraverseUtil.isSkipped(nodes[i])) { | |
684 return true; | |
685 } | |
686 var style = window.getComputedStyle(nodes[i], null); | |
687 if (style && (style.display != 'inline' || | |
688 breakTags[nodes[i].tagName])) { | |
689 return true; | |
690 } | |
691 } | |
692 return false; | |
693 }); | |
694 }; | |
695 | |
696 /** | |
697 * Finds the next paragraph, starting from endCursor. Upon exit, | |
698 * startCursor and endCursor will surround the next paragraph. | |
699 * | |
700 * @param {Cursor} startCursor On exit, marks the beginning of the paragraph. | |
701 * @param {Cursor} endCursor The position to start searching for the next | |
702 * paragraph. On exit, will point to the end of the returned string. | |
703 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
704 * initial and final cursor position will be pushed onto this array. | |
705 * @return {?string} The next paragraph, or null if the bottom of the | |
706 * document has been reached. | |
707 */ | |
708 TraverseUtil.getNextParagraph = function(startCursor, endCursor, | |
709 nodesCrossed) { | |
710 return TraverseUtil.getNextString( | |
711 startCursor, endCursor, nodesCrossed, | |
712 function(str, word, nodes) { | |
713 for (var i = 0; i < nodes.length; i++) { | |
714 if (TraverseUtil.isSkipped(nodes[i])) { | |
715 return true; | |
716 } | |
717 var style = window.getComputedStyle(nodes[i], null); | |
718 if (style && style.display != 'inline') { | |
719 return true; | |
720 } | |
721 } | |
722 return false; | |
723 }); | |
724 }; | |
725 | |
726 /** | |
727 * Finds the previous paragraph, starting from startCursor. Upon exit, | |
728 * startCursor and endCursor will surround the previous paragraph. | |
729 * | |
730 * @param {Cursor} startCursor The position to start searching for the next | |
731 * paragraph. On exit, will point to the start of the returned string. | |
732 * @param {Cursor} endCursor On exit, the end of the returned string. | |
733 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
734 * initial and final cursor position will be pushed onto this array. | |
735 * @return {?string} The previous paragraph, or null if the bottom of the | |
736 * document has been reached. | |
737 */ | |
738 TraverseUtil.getPreviousParagraph = function( | |
739 startCursor, endCursor, nodesCrossed) { | |
740 return TraverseUtil.getPreviousString( | |
741 startCursor, endCursor, nodesCrossed, | |
742 function(str, word, nodes) { | |
743 for (var i = 0; i < nodes.length; i++) { | |
744 if (TraverseUtil.isSkipped(nodes[i])) { | |
745 return true; | |
746 } | |
747 var style = window.getComputedStyle(nodes[i], null); | |
748 if (style && style.display != 'inline') { | |
749 return true; | |
750 } | |
751 } | |
752 return false; | |
753 }); | |
754 }; | |
755 | |
756 /** | |
757 * Customizable function to return the next string of words in the DOM, based | |
758 * on provided functions to decide when to break one string and start | |
759 * the next. This can be used to get the next sentence, line, paragraph, | |
760 * or potentially other granularities. | |
761 * | |
762 * Finds the next contiguous string, starting from endCursor. Upon exit, | |
763 * startCursor and endCursor will surround the next string. | |
764 * | |
765 * The breakBefore function takes three parameters, and | |
766 * should return true if the string should be broken before the proposed | |
767 * next word: | |
768 * str The string so far. | |
769 * word The next word to be added. | |
770 * nodesCrossed The nodes crossed in reaching this next word. | |
771 * | |
772 * @param {Cursor} startCursor On exit, will point to the beginning of the | |
773 * next string. | |
774 * @param {Cursor} endCursor The position to start searching for the next | |
775 * string. On exit, will point to the end of the returned string. | |
776 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
777 * initial and final cursor position will be pushed onto this array. | |
778 * @param {function(string, string, Array.<string>)} breakBefore | |
779 * Function that takes the string so far, next word to be added, and | |
780 * nodes crossed, and returns true if the string should be ended before | |
781 * adding this word. | |
782 * @return {?string} The next string, or null if the bottom of the | |
783 * document has been reached. | |
784 */ | |
785 TraverseUtil.getNextString = function( | |
786 startCursor, endCursor, nodesCrossed, breakBefore) { | |
787 // Get the first word and set the start cursor to the start of the | |
788 // first word. | |
789 var wordStartCursor = endCursor.clone(); | |
790 var wordEndCursor = endCursor.clone(); | |
791 var newNodesCrossed = []; | |
792 var str = ''; | |
793 var word = TraverseUtil.getNextWord( | |
794 wordStartCursor, wordEndCursor, newNodesCrossed); | |
795 if (word == null) | |
796 return null; | |
797 startCursor.copyFrom(wordStartCursor); | |
798 | |
799 // Always add the first word when the string is empty, and then keep | |
800 // adding more words as long as breakBefore returns false | |
801 while (!str || !breakBefore(str, word, newNodesCrossed)) { | |
802 // Append this word, set the end cursor to the end of this word, and | |
803 // update the returned list of nodes crossed to include ones we crossed | |
804 // in reaching this word. | |
805 if (str) | |
806 str += ' '; | |
807 str += word; | |
808 nodesCrossed = nodesCrossed.concat(newNodesCrossed); | |
809 endCursor.copyFrom(wordEndCursor); | |
810 | |
811 // Get the next word and go back to the top of the loop. | |
812 newNodesCrossed = []; | |
813 word = TraverseUtil.getNextWord( | |
814 wordStartCursor, wordEndCursor, newNodesCrossed); | |
815 if (word == null) | |
816 return str; | |
817 } | |
818 | |
819 return str; | |
820 }; | |
821 | |
822 /** | |
823 * Customizable function to return the previous string of words in the DOM, | |
824 * based on provided functions to decide when to break one string and start | |
825 * the next. See getNextString, above, for more details. | |
826 * | |
827 * Finds the previous contiguous string, starting from startCursor. Upon exit, | |
828 * startCursor and endCursor will surround the next string. | |
829 * | |
830 * @param {Cursor} startCursor The position to start searching for the | |
831 * previous string. On exit, will point to the beginning of the | |
832 * string returned. | |
833 * @param {Cursor} endCursor On exit, will point to the end of the | |
834 * string returned. | |
835 * @param {Array.<Node>} nodesCrossed Any HTML nodes crossed between the | |
836 * initial and final cursor position will be pushed onto this array. | |
837 * @param {function(string, string, Array.<string>)} breakBefore | |
838 * Function that takes the string so far, the word to be added, and | |
839 * nodes crossed, and returns true if the string should be ended before | |
840 * adding this word. | |
841 * @return {?string} The next string, or null if the top of the | |
842 * document has been reached. | |
843 */ | |
844 TraverseUtil.getPreviousString = function( | |
845 startCursor, endCursor, nodesCrossed, breakBefore) { | |
846 // Get the first word and set the end cursor to the end of the | |
847 // first word. | |
848 var wordStartCursor = startCursor.clone(); | |
849 var wordEndCursor = startCursor.clone(); | |
850 var newNodesCrossed = []; | |
851 var str = ''; | |
852 var word = TraverseUtil.getPreviousWord( | |
853 wordStartCursor, wordEndCursor, newNodesCrossed); | |
854 if (word == null) | |
855 return null; | |
856 endCursor.copyFrom(wordEndCursor); | |
857 | |
858 // Always add the first word when the string is empty, and then keep | |
859 // adding more words as long as breakBefore returns false | |
860 while (!str || !breakBefore(str, word, newNodesCrossed)) { | |
861 // Prepend this word, set the start cursor to the start of this word, and | |
862 // update the returned list of nodes crossed to include ones we crossed | |
863 // in reaching this word. | |
864 if (str) | |
865 str = ' ' + str; | |
866 str = word + str; | |
867 nodesCrossed = nodesCrossed.concat(newNodesCrossed); | |
868 startCursor.copyFrom(wordStartCursor); | |
869 v | |
870 // Get the previous word and go back to the top of the loop. | |
871 newNodesCrossed = []; | |
872 word = TraverseUtil.getPreviousWord( | |
873 wordStartCursor, wordEndCursor, newNodesCrossed); | |
874 if (word == null) | |
875 return str; | |
876 } | |
877 | |
878 return str; | |
879 }; | |
OLD | NEW |