text: // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  //
  // 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.
  
  
  goog.provide('goog.debug.DevCss');
  goog.provide('goog.debug.DevCss.UserAgent');
  
  goog.require('goog.cssom');
  goog.require('goog.dom.classes');
  goog.require('goog.events');
  goog.require('goog.events.EventType');
  goog.require('goog.string');
  goog.require('goog.userAgent');
  
  
  
  /**
   * A class for solving development CSS issues/emulating the CSS Compiler.
   * @param {goog.debug.DevCss.UserAgent=} opt_userAgent The user agent, if not
   *     passed in, will be determined using goog.userAgent.
   * @param {number|string=} opt_userAgentVersion The user agent's version.
   *     If not passed in, will be determined using goog.userAgent.
   * @throws {Error} When userAgent detection fails.
   * @constructor
   */
  goog.debug.DevCss = function(opt_userAgent, opt_userAgentVersion) {
    if (!opt_userAgent) {
      // Walks through the known goog.userAgents.
      if (goog.userAgent.IE) {
        opt_userAgent = goog.debug.DevCss.UserAgent.IE;
      } else if (goog.userAgent.GECKO) {
        opt_userAgent = goog.debug.DevCss.UserAgent.GECKO;
      } else if (goog.userAgent.WEBKIT) {
        opt_userAgent = goog.debug.DevCss.UserAgent.WEBKIT;
      } else if (goog.userAgent.MOBILE) {
        opt_userAgent = goog.debug.DevCss.UserAgent.MOBILE;
      } else if (goog.userAgent.OPERA) {
        opt_userAgent = goog.debug.DevCss.UserAgent.OPERA;
      }
    }
    switch (opt_userAgent) {
      case goog.debug.DevCss.UserAgent.OPERA:
      case goog.debug.DevCss.UserAgent.IE:
      case goog.debug.DevCss.UserAgent.GECKO:
      case goog.debug.DevCss.UserAgent.FIREFOX:
      case goog.debug.DevCss.UserAgent.WEBKIT:
      case goog.debug.DevCss.UserAgent.SAFARI:
      case goog.debug.DevCss.UserAgent.MOBILE:
        break;
      default:
        throw Error('Could not determine the user agent from known UserAgents');
    }
  
    /**
     * One of goog.debug.DevCss.UserAgent.
     * @type {string}
     * @private
     */
    this.userAgent_ = opt_userAgent;
  
    /**
     * @type {number|string}
     * @private
     */
    this.userAgentVersion_ = opt_userAgentVersion || goog.userAgent.VERSION;
    this.generateUserAgentTokens_();
  
    /**
     * @type {boolean}
     * @private
     */
    this.isIe6OrLess_ = this.userAgent_ == goog.debug.DevCss.UserAgent.IE &&
        goog.string.compareVersions('7', this.userAgentVersion_) > 0;
  
    if (this.isIe6OrLess_) {
      /**
       * @type {Array.<{classNames,combinedClassName,els}>}
       * @private
       */
      this.ie6CombinedMatches_ = [];
    }
  };
  
  
  /**
   * Rewrites the CSSOM as needed to activate any useragent-specific selectors.
   * @param {boolean=} opt_enableIe6ReadyHandler If true(the default), and the
   *     userAgent is ie6, we set a document "ready" event handler to walk the DOM
   *     and make combined selector className changes. Having this parameter also
   *     aids unit testing.
   */
  goog.debug.DevCss.prototype.activateBrowserSpecificCssRules = function(
      opt_enableIe6ReadyHandler) {
    var enableIe6EventHandler = goog.isDef(opt_enableIe6ReadyHandler) ?
        opt_enableIe6ReadyHandler : true;
    var cssRules = goog.cssom.getAllCssStyleRules();
  
    for (var i = 0, cssRule; cssRule = cssRules[i]; i++) {
      this.replaceBrowserSpecificClassNames_(cssRule);
    }
  
    // Since we may have manipulated the rules above, we'll have to do a
    // complete sweep again if we're in IE6. Luckily performance doesn't
    // matter for this tool.
    if (this.isIe6OrLess_) {
      cssRules = goog.cssom.getAllCssStyleRules();
      for (var i = 0, cssRule; cssRule = cssRules[i]; i++) {
        this.replaceIe6CombinedSelectors_(cssRule);
      }
    }
  
    // Add an event listener for document ready to rewrite any necessary
    // combined classnames in IE6.
    if (this.isIe6OrLess_ && enableIe6EventHandler) {
      goog.events.listen(document, goog.events.EventType.LOAD, goog.bind(
          this.addIe6CombinedClassNames_, this));
    }
  };
  
  
  /**
   * @type {Object}
   * @private
   */
  goog.debug.DevCss.prototype.userAgentTokens_ = {};
  
  
  /**
   * A list of possible user agent strings.
   * @enum {string}
   */
  goog.debug.DevCss.UserAgent = {
    OPERA: 'OPERA',
    IE: 'IE',
    GECKO: 'GECKO',
    FIREFOX: 'GECKO',
    WEBKIT: 'WEBKIT',
    SAFARI: 'WEBKIT',
    MOBILE: 'MOBILE'
  };
  
  
  /**
   * A list of strings that may be used for matching in CSS files/development.
   * @enum {string}
   * @private
   */
  goog.debug.DevCss.CssToken_ = {
    USERAGENT: 'USERAGENT',
    SEPARATOR: '-',
    LESS_THAN: 'LT',
    GREATER_THAN: 'GT',
    LESS_THAN_OR_EQUAL: 'LTE',
    GREATER_THAN_OR_EQUAL: 'GTE',
    IE6_SELECTOR_TEXT: 'goog-ie6-selector',
    IE6_COMBINED_GLUE: '_'
  };
  
  
  /**
   * Generates user agent token match strings with comparison and version bits.
   * For example:
   *   userAgentTokens_.ANY will be like 'GECKO'
   *   userAgentTokens_.LESS_THAN will be like 'GECKO-LT3' etc...
   * @private
   */
  goog.debug.DevCss.prototype.generateUserAgentTokens_ = function() {
    this.userAgentTokens_.ANY = goog.debug.DevCss.CssToken_.USERAGENT +
        goog.debug.DevCss.CssToken_.SEPARATOR + this.userAgent_;
    this.userAgentTokens_.EQUALS = this.userAgentTokens_.ANY +
        goog.debug.DevCss.CssToken_.SEPARATOR;
    this.userAgentTokens_.LESS_THAN = this.userAgentTokens_.ANY +
        goog.debug.DevCss.CssToken_.SEPARATOR +
        goog.debug.DevCss.CssToken_.LESS_THAN;
    this.userAgentTokens_.LESS_THAN_OR_EQUAL = this.userAgentTokens_.ANY +
        goog.debug.DevCss.CssToken_.SEPARATOR +
        goog.debug.DevCss.CssToken_.LESS_THAN_OR_EQUAL;
    this.userAgentTokens_.GREATER_THAN = this.userAgentTokens_.ANY +
        goog.debug.DevCss.CssToken_.SEPARATOR +
        goog.debug.DevCss.CssToken_.GREATER_THAN;
    this.userAgentTokens_.GREATER_THAN_OR_EQUAL = this.userAgentTokens_.ANY +
        goog.debug.DevCss.CssToken_.SEPARATOR +
        goog.debug.DevCss.CssToken_.GREATER_THAN_OR_EQUAL;
  };
  
  
  /**
   * Gets the version number bit from a selector matching userAgentToken.
   * @param {string} selectorText The selector text of a CSS rule.
   * @param {string} userAgentToken Includes the LTE/GTE bit to see if it matches.
   * @return {string|undefined} The version number.
   * @private
   */
  goog.debug.DevCss.prototype.getVersionNumberFromSelectorText_ = function(
      selectorText, userAgentToken) {
    var regex = new RegExp(userAgentToken + '([\\d\\.]+)');
    var matches = regex.exec(selectorText);
    if (matches && matches.length == 2) {
      return matches[1];
    }
  };
  
  
  /**
   * Extracts a rule version from the selector text, and if it finds one, calls
   * compareVersions against it and the passed in token string to provide the
   * value needed to determine if we have a match or not.
   * @param {CSSRule} cssRule The rule to test against.
   * @param {string} token The match token to test against the rule.
   * @return {Array|undefined} A tuple with the result of the compareVersions call
   *     and the matched ruleVersion.
   * @private
   */
  goog.debug.DevCss.prototype.getRuleVersionAndCompare_ = function(cssRule,
      token) {
    if (!cssRule.selectorText.match(token)) {
      return;
    }
    var ruleVersion = this.getVersionNumberFromSelectorText_(
        cssRule.selectorText, token);
    if (!ruleVersion) {
      return;
    }
  
    var comparison = goog.string.compareVersions(this.userAgentVersion_,
        ruleVersion);
    return [comparison, ruleVersion];
  };
  
  
  /**
   * Replaces a CSS selector if we have matches based on our useragent/version.
   * Example: With a selector like ".USERAGENT-IE-LTE6 .class { prop: value }" if
   * we are running IE6 we'll end up with ".class { prop: value }", thereby
   * "activating" the selector.
   * @param {CSSRule} cssRule The cssRule to potentially replace.
   * @private
   */
  goog.debug.DevCss.prototype.replaceBrowserSpecificClassNames_ = function(
      cssRule) {
  
    // If we don't match the browser token, we can stop now.
    if (!cssRule.selectorText.match(this.userAgentTokens_.ANY)) {
      return;
    }
  
    // We know it will begin as a classname.
    var additionalRegexString;
  
    // Tests "Less than or equals".
    var compared = this.getRuleVersionAndCompare_(cssRule,
        this.userAgentTokens_.LESS_THAN_OR_EQUAL);
    if (compared && compared.length) {
      if (compared[0] > 0) {
        return;
      }
      additionalRegexString = this.userAgentTokens_.LESS_THAN_OR_EQUAL +
          compared[1];
    }
  
    // Tests "Less than".
    compared = this.getRuleVersionAndCompare_(cssRule,
        this.userAgentTokens_.LESS_THAN);
    if (compared && compared.length) {
      if (compared[0] > -1) {
        return;
      }
      additionalRegexString = this.userAgentTokens_.LESS_THAN + compared[1];
    }
  
    // Tests "Greater than or equals".
    compared = this.getRuleVersionAndCompare_(cssRule,
        this.userAgentTokens_.GREATER_THAN_OR_EQUAL);
    if (compared && compared.length) {
      if (compared[0] < 0) {
        return;
      }
      additionalRegexString = this.userAgentTokens_.GREATER_THAN_OR_EQUAL +
          compared[1];
    }
  
    // Tests "Greater than".
    compared = this.getRuleVersionAndCompare_(cssRule,
        this.userAgentTokens_.GREATER_THAN);
    if (compared && compared.length) {
      if (compared[0] < 1) {
        return;
      }
      additionalRegexString = this.userAgentTokens_.GREATER_THAN + compared[1];
    }
  
    // Tests "Equals".
    compared = this.getRuleVersionAndCompare_(cssRule,
        this.userAgentTokens_.EQUALS);
    if (compared && compared.length) {
      if (compared[0] != 0) {
        return;
      }
      additionalRegexString = this.userAgentTokens_.EQUALS + compared[1];
    }
  
    // If we got to here without generating the additionalRegexString, then
    // we did not match any of our comparison token strings, and we want a
    // general browser token replacement.
    if (!additionalRegexString) {
      additionalRegexString = this.userAgentTokens_.ANY;
    }
  
    // We need to match at least a single whitespace character to know that
    // we are matching the entire useragent string token.
    var regexString = '\\.' + additionalRegexString + '\\s+';
    var re = new RegExp(regexString, 'g');
  
    var currentCssText = goog.cssom.getCssTextFromCssRule(cssRule);
  
    // Replacing the token with '' activates the selector for this useragent.
    var newCssText = currentCssText.replace(re, '');
  
    if (newCssText != currentCssText) {
      goog.cssom.replaceCssRule(cssRule, newCssText);
    }
  };
  
  
  /**
   * Replaces IE6 combined selector rules with a workable development alternative.
   * IE6 actually parses .class1.class2 {} to simply .class2 {} which is nasty.
   * To fully support combined selectors in IE6 this function needs to be paired
   * with a call to replace the relevant DOM elements classNames as well.
   * @see {this.addIe6CombinedClassNames_}
   * @param {CSSRule} cssRule The rule to potentially fix.
   * @private
   */
  goog.debug.DevCss.prototype.replaceIe6CombinedSelectors_ = function(cssRule) {
    // This match only ever works in IE because other UA's won't have our
    // IE6_SELECTOR_TEXT in the cssText property.
    if (cssRule.style.cssText &&
        cssRule.style.cssText.match(
            goog.debug.DevCss.CssToken_.IE6_SELECTOR_TEXT)) {
      var cssText = goog.cssom.getCssTextFromCssRule(cssRule);
      var combinedSelectorText = this.getIe6CombinedSelectorText_(cssText);
      if (combinedSelectorText) {
        var newCssText = combinedSelectorText + '{' + cssRule.style.cssText + '}';
        goog.cssom.replaceCssRule(cssRule, newCssText);
      }
    }
  };
  
  
  /**
   * Gets the appropriate new combined selector text for IE6.
   * Also adds an entry onto ie6CombinedMatches_ with relevant info for the
   * likely following call to walk the DOM and rewrite the class attribute.
   * Example: With a selector like
   *     ".class2 { -goog-ie6-selector: .class1.class2; prop: value }".
   * this function will return:
   *     ".class1_class2 { prop: value }".
   * @param {string} cssText The CSS selector text and css rule text combined.
   * @return {?string} The rewritten css rule text.
   * @private
   */
  goog.debug.DevCss.prototype.getIe6CombinedSelectorText_ = function(cssText) {
    var regex = new RegExp(goog.debug.DevCss.CssToken_.IE6_SELECTOR_TEXT +
        '\\s*:\\s*\\"([^\\"]+)\\"', 'gi');
    var matches = regex.exec(cssText);
    if (matches) {
      var combinedSelectorText = matches[1];
      // To aid in later fixing the DOM, we need to split up the possible
      // selector groups by commas.
      var groupedSelectors = combinedSelectorText.split(/\s*\,\s*/);
      for (var i = 0, selector; selector = groupedSelectors[i]; i++) {
        // Strips off the leading ".".
        var combinedClassName = selector.substr(1);
        var classNames = combinedClassName.split(
            goog.debug.DevCss.CssToken_.IE6_COMBINED_GLUE);
        var entry = {
          classNames: classNames,
          combinedClassName: combinedClassName,
          els: []
        };
        this.ie6CombinedMatches_.push(entry);
      }
      return combinedSelectorText;
    }
    return null;
  };
  
  
  /**
   * Adds combined selectors with underscores to make them "work" in IE6.
   * @see {this.replaceIe6CombinedSelectors_}
   * @private
   */
  goog.debug.DevCss.prototype.addIe6CombinedClassNames_ = function() {
    if (!this.ie6CombinedMatches_.length) {
      return;
    }
    var allEls = document.getElementsByTagName('*');
    var matches = [];
    // Match nodes for all classNames.
    for (var i = 0, classNameEntry; classNameEntry =
        this.ie6CombinedMatches_[i]; i++) {
      for (var j = 0, el; el = allEls[j]; j++) {
        var classNamesLength = classNameEntry.classNames.length;
        for (var k = 0, className; className = classNameEntry.classNames[k];
            k++) {
          if (!goog.dom.classes.has(el, className)) {
            break;
          }
          if (k == classNamesLength - 1) {
            classNameEntry.els.push(el);
          }
        }
      }
      // Walks over our matching nodes and fixes them.
      if (classNameEntry.els.length) {
        for (var j = 0, el; el = classNameEntry.els[j]; j++) {
          if (!goog.dom.classes.has(el, classNameEntry.combinedClassName)) {
            goog.dom.classes.add(el, classNameEntry.combinedClassName);
          }
        }
      }
    }
  };
  
  