| // Licensed to the Software Freedom Conservancy (SFC) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The SFC licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| /** |
| * @fileoverview Text handling methods. |
| */ |
| |
| goog.provide('core.text'); |
| |
| |
| goog.require('bot'); |
| goog.require('bot.events'); |
| goog.require('bot.userAgent'); |
| goog.require('core.patternMatcher'); |
| goog.require('goog.dom'); |
| goog.require('goog.dom.NodeType'); |
| goog.require('goog.string'); |
| goog.require('goog.userAgent'); |
| |
| var SHADOW_DOM_ENABLED = typeof ShadowRoot === 'function'; |
| |
| /** |
| * Attempt to normalize the text content of an element. |
| * |
| * @param {!Node} element The element to use. |
| * @param {boolean} preformatted Whether the text is preformatted or not. |
| * @return {string} The text content of the element. |
| * @private |
| */ |
| core.text.getTextContent_ = function(element, preformatted) { |
| if (element.style && (element.style.visibility == 'hidden' || |
| element.style.display == 'none')) { |
| return ''; |
| } |
| |
| var text; |
| |
| if (element.nodeType == goog.dom.NodeType.TEXT) { |
| text = element.data; |
| if (!preformatted) { |
| text = text.replace(/\n|\r|\t/g, ' '); |
| } |
| return text.replace(/ /, ' '); |
| } |
| if (SHADOW_DOM_ENABLED && |
| element.nodeType == goog.dom.NodeType.ELEMENT && |
| element.shadowRoot !== null) { |
| return core.text.getTextContent_(element.shadowRoot, preformatted); |
| } |
| if ((element.nodeType == goog.dom.NodeType.ELEMENT || |
| element.nodeType == goog.dom.NodeType.DOCUMENT_FRAGMENT) && |
| element.nodeName != 'SCRIPT' && |
| element.nodeName != 'STYLE') { |
| var childrenPreformatted = preformatted || (element.tagName == 'PRE'); |
| text = ''; |
| for (var i = 0; i < element.childNodes.length; i++) { |
| var child = element.childNodes.item(i); |
| if (!child) { |
| continue; |
| } |
| if (SHADOW_DOM_ENABLED && |
| (child.nodeName == 'CONTENT' || child.nodeName == 'SLOT')) { |
| var shadowChildren; |
| if (child.nodeName == 'CONTENT') { |
| shadowChildren = child.getDistributedNodes(); |
| } else { |
| shadowChildren = child.assignedNodes(); |
| } |
| for (var j = 0; j < shadowChildren.length; j++) { |
| var shadowChild = shadowChildren[j]; |
| if (!shadowChild) { |
| continue; |
| } |
| text += core.text.getTextContent_(shadowChild, preformatted); |
| } |
| continue; |
| } |
| text += core.text.getTextContent_(child, childrenPreformatted); |
| } |
| // Handle block elements that introduce newlines |
| // -- From HTML spec: |
| //<!ENTITY % block |
| // "P | %heading; | %list; | %preformatted; | DL | DIV | NOSCRIPT | |
| // BLOCKQUOTE | F:wORM | HR | TABLE | FIELDSET | ADDRESS"> |
| // |
| // TODO: should potentially introduce multiple newlines to separate blocks |
| if (element.tagName == 'P' || element.tagName == 'BR' || |
| element.tagName == 'HR' || element.tagName == 'DIV') { |
| text += '\n'; |
| } |
| text = text.replace(/ /, ' '); |
| if (bot.userAgent.IE_DOC_9) { |
| text = text.replace(/d/, ' '); |
| } |
| return text; |
| } |
| return ''; |
| }; |
| |
| |
| /** |
| * Convert all newlines to \n. A newline is defined as being one of the common |
| * line endings for Mac, Windows or UNIX. |
| * |
| * @param {string} text The text to normalize. |
| * @return {string} The converted text, with all line endings replaced |
| * with '\n'. |
| * @private |
| */ |
| core.text.normalizeNewlines_ = function(text) { |
| return text.replace(/\r\n|\r/g, '\n'); |
| }; |
| |
| |
| /** |
| * @param {string} text The text to perform the replacement on. |
| * @param {string} oldText The string to replace. |
| * @param {string} newText The replacement text. |
| * @return {string} 'text' with all occurances of 'oldText' replaced by |
| * 'newText'. |
| * @private |
| */ |
| core.text.replaceAll_ = function(text, oldText, newText) { |
| while (text.indexOf(oldText) != -1) { |
| text = text.replace(oldText, newText); |
| } |
| return text; |
| }; |
| |
| |
| /** |
| * Replace multiple sequential spaces with a single space, and then convert |
| * to space. |
| * |
| * @param {string} text The text to normalize. |
| * @return {string} The normalized text. |
| * @private |
| */ |
| core.text.normalizeSpaces_ = function(text) { |
| // Replace multiple spaces with a single space |
| // TODO - this shouldn't occur inside PRE elements |
| text = text.replace(/\ +/g, ' '); |
| |
| // Replace with a space |
| var nbspPattern = new RegExp(String.fromCharCode(160), 'g'); |
| if (goog.userAgent.WEBKIT) { |
| return core.text.replaceAll_(text, String.fromCharCode(160), ' '); |
| } else { |
| return text.replace(nbspPattern, ' '); |
| } |
| }; |
| |
| |
| /** |
| * Return an element's text content. |
| * |
| * @param {!Element} element The element whose text to retrieve. |
| * @return {string} The element's text content. |
| */ |
| core.text.getElementText = function(element) { |
| var text = ''; |
| var isRecentFirefox = |
| (goog.userAgent.GECKO && goog.userAgent.VERSION >= '1.8'); |
| |
| if (isRecentFirefox || goog.userAgent.WEBKIT || goog.userAgent.IE) { |
| text = core.text.getTextContent_(element, false); |
| } else { |
| if (element.textContent) { |
| text = element.textContent; |
| } else { |
| if (element.innerText) { |
| text = element.innerText; |
| } |
| } |
| } |
| |
| text = core.text.normalizeNewlines_(text); |
| text = core.text.normalizeSpaces_(text); |
| |
| return goog.string.trim(text); |
| }; |
| |
| /** |
| * @return {string} The entire text content of the page. |
| */ |
| core.text.getBodyText = function() { |
| var doc = bot.getWindow().document; |
| var body = doc.body; |
| return !!body ? core.text.getElementText(body) : ''; |
| }; |
| |
| |
| /** |
| * Verifies that the specified text pattern appears somewhere on the rendered |
| * page shown to the user. |
| * |
| * @param {string} pattern A <a href="#patterns">pattern</a> to match with the |
| * text of the page. |
| * @return {boolean} Whether the pattern matches the text. |
| */ |
| core.text.isTextPresent = function(pattern) { |
| var allText = core.text.getBodyText(); |
| |
| var patternMatcher = core.patternMatcher.against(pattern); |
| if (patternMatcher.strategyName == 'glob') { |
| if (pattern.indexOf('glob:') == 0) { |
| pattern = pattern.substring('glob:'.length); // strip off "glob:" |
| } |
| patternMatcher = core.patternMatcher.against('globContains:' + pattern); |
| } |
| return patternMatcher(allText); |
| }; |
| |
| |
| /** |
| * @param {string} locator The link text to search for. |
| * @param {Document=} opt_doc The document to search in. |
| * @return {Element} The located element or {@code null}. |
| */ |
| core.text.linkLocator = function(locator, opt_doc) { |
| var doc = opt_doc || goog.dom.getOwnerDocument(bot.getWindow()); |
| |
| var links = doc.getElementsByTagName('a'); |
| |
| for (var i = 0; i < links.length; i++) { |
| var element = links[i]; |
| var text = core.text.getElementText(element); |
| if (core.patternMatcher.matches(locator, text)) { |
| return element; |
| } |
| } |
| return null; |
| }; |
| |
| |
| /** |
| * Set a new caret position within an element. |
| * |
| * @param {!Element} element The element to use. |
| * @param {number} position The new caret position. |
| */ |
| core.text.setCursorPosition = function(element, position) { |
| if (position == -1) { |
| position = element.value.length; |
| } |
| |
| if (element.setSelectionRange) { |
| element.focus(); |
| element.setSelectionRange(/*start*/ position, /*end*/ position); |
| } else if (element.createTextRange) { |
| bot.events.fire(element, bot.events.EventType.FOCUS); |
| var range = element.createTextRange(); |
| range.collapse(true); |
| range.moveEnd('character', position); |
| range.moveStart('character', position); |
| range.select(); |
| } |
| }; |
| |