text: /*
   * Copyright 2014 Google Inc.
   *
   * Licensed 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 JS class for finding below-the-fold XPaths used by split_html.
   */
  
  goog.provide('pagespeedutils.CriticalXPaths');
  goog.provide('pagespeedutils.generateXPath');
  
  goog.require('pagespeedutils');
  
  
  
  /**
   * @constructor
   * @param {number} viewportWidth Width of the viewport.
   * @param {number} viewportHeight Height of the viewport.
   * @param {Node} document The root document.
   */
  pagespeedutils.CriticalXPaths = function(
      viewportWidth, viewportHeight, document) {
    /**
     * Viewport size.
     * @type {{ height: (number), width: (number) }}
     * @private
     */
    this.windowSize_ = {
      'height': viewportHeight,
      'width': viewportWidth
    };
  
    /**
     * List of xpath pairs in the form start_xpath:end_xpath.
     * @type {Array.<string>}
     * @private
     */
    this.xpathPairs_ = [];
  
    this.document_ = document;
  };
  
  
  /**
   * Gets the start and end xpath for the non-critical panels.
   * @return {Array.<string>}
   */
  pagespeedutils.CriticalXPaths.prototype.getNonCriticalPanelXPathPairs =
      function() {
    // We only need to mark elements in the body. Elements in the head are never
    // visible.
    this.findNonCriticalPanelBoundaries_(this.document_.body);
  
    return this.xpathPairs_;
  };
  
  
  /**
   * Appends Appends a new XPath to the current list of XPath pairs.
   * @param {string} startXpath Start XPath.
   * @param {string} endXpath End XPath.
   * @private
   */
  pagespeedutils.CriticalXPaths.prototype.addXPathPair_ = function(startXpath,
      endXpath) {
    if (!startXpath) {
      return;
    }
    var xpathPair = startXpath;
    if (endXpath) {
      xpathPair += ':' + endXpath;
    }
    this.xpathPairs_.push(xpathPair);
  };
  
  
  /**
   * Identifies all nodes which are not in viewport and populates the start and
   * end xpath of non-critical panels into xpathPairs_.
   * @param  {!Node} node Dom node.
   * @return {boolean} true iff node in viewport or any of node's
   * descendants is critical.
   * @private
   */
  pagespeedutils.CriticalXPaths.prototype.findNonCriticalPanelBoundaries_ =
      function(node) {
    var nodeIsCritical = pagespeedutils.inViewport(node, this.windowSize_);
    var prevChildIsCritical = nodeIsCritical;
    // The loop below determines the XPaths at the transition points of nodes
    // going from critical to non-critical. startChildXPath will be the first
    // non-critical node, and endChildXPath will be the first critical node after
    // the non-critical section.
    var startChildXPath = '';
    var endChildXPath = '';
    // Iterate over the visible (that is, not hidden, independent of viewport
    // size) sibling nodes of the passed in node.
    var firstChild = this.visibleNodeOrSibling_(node.firstChild);
    for (var currNode = firstChild; currNode != null;
         currNode = this.visibleNodeOrSibling_(currNode.nextSibling)) {
      // Check if we are at a criticality transition point by making a recursive
      // call to findNonCriticalPanelBoundaries for the current sibling node. If
      // we are at a transition point from critical to non-critical, we need to
      // mark the current sibling node as the start of an XPath. If we are
      // transitioning from non-critical to critical, then the current node is the
      // end of our XPath, and we need to store the complete path.
      // TODO(jud): The logic here is tricky to follow. See if we can simplify
      // this to make it clearer and more efficient.
      var currNodeIsCritical = this.findNonCriticalPanelBoundaries_(currNode);
      if (currNodeIsCritical != prevChildIsCritical) {
        if (!currNodeIsCritical) {
          // Going from critical -> non-critical.
          startChildXPath = pagespeedutils.generateXPath(
              currNode, this.document_);
          endChildXPath = '';
        } else {
          // Going from non-critical -> critical
          if (!nodeIsCritical) {
            // But non-critical panel never began.
            // All the nodes till currNode are non-critical.
            if (firstChild != currNode) {
              startChildXPath = pagespeedutils.generateXPath(
                  firstChild, this.document_);
            }
            nodeIsCritical = true;
          }
          endChildXPath = pagespeedutils.generateXPath(currNode, this.document_);
          if (!endChildXPath) {
            // If 'endChildXPath' is empty here, then it is better not to split
            // here than treating few critical nodes as non-critical.
            startChildXPath = '';
          }
          if (startChildXPath) {
            this.addXPathPair_(startChildXPath, endChildXPath);
          }
          startChildXPath = '';
        }
        prevChildIsCritical = currNodeIsCritical;
      }
    }
    if (startChildXPath) {
      this.addXPathPair_(startChildXPath, endChildXPath);
    }
    return nodeIsCritical;
  };
  
  
  /**
   * Generates the xpath for the given node which is relative to body.
   * @param {Node} node Node in dom tree whose xpath is generated.
   * @param {Node} doc Page document.
   * @return {string} xpath of a node which is always a rendered one.
   */
  pagespeedutils.generateXPath = function(node, doc) {
    var xpathUnits = [];
    while (node != doc.body) {
      var id = node.getAttribute('id');
      if (id && doc.querySelectorAll('#' + id).length == 1) {
        // Use 'id' inside xpath only if it is used once in the document.
        xpathUnits.unshift(node.tagName.toLowerCase() + '[@id=\"' +
            node.getAttribute('id') + '\"]');
        break;
      } else {
        var i = 0;
        for (var sibling = node;
             sibling;
             sibling = sibling.previousElementSibling) {
          // Please keep this list of tags same as that in
          // google_critical_line_info_finder.cc.
          if ((sibling.tagName === 'SCRIPT') ||
              (sibling.tagName === 'NOSCRIPT') ||
              (sibling.tagName === 'STYLE') ||
              (sibling.tagName === 'LINK')) {
            continue;
          }
          ++i;
        }
        xpathUnits.unshift(node.tagName.toLowerCase() + '[' + i + ']');
      }
      node = node.parentNode;
    }
    return xpathUnits.length ? xpathUnits.join('/') : '';
  };
  
  
  /**
   * @param {Node} node Node in DOM tree.
   * @return {?Node} Returns node if it is visible (as in width and height greater
   *     than 0, irrespective of the viewport), or the first visible sibling of
   *     node if it isn't. Returns null if no valid nodes are found.
   * @private
   */
  pagespeedutils.CriticalXPaths.prototype.visibleNodeOrSibling_ = function(node) {
    for (; node != null; node = node.nextSibling) {
      if (node.nodeType != Node.ELEMENT_NODE) continue;
      if (node.offsetWidth == 0 && node.offsetHeight == 0) continue;
      // Also check node.offsetParent to see if the node is hidden. offsetParent
      // is null when the node has display:none or position:fixed styling, or if
      // it's a body node. To differentiate, we explicitly check for
      // position:fixed and body and do not consider those cases hidden.
      if (node.offsetParent || node.tagName == 'BODY' ||
          node.style.position == 'fixed') return node;
    }
    return null;
  };
  