// css parsing plugin for jQuery.
// Either scans the entire document for <link type='text/css'> and <style> elements
// and returns the CSS from that, or parses a passed-in string.

// Some jargon : in 'foo { bar: quux }' , the whole thing is the statement, 'foo' is the selector, '{bar: quux}' is the cssText, 
// 'bar: quux' is the declaration, 'bar' is the attribute and 'quux' is the value

// Returns an object with the keys being the selector and the values being the parsed cssText.
// the parsed cssText is an object with the keys being the attribute and the value being the css value, but
// any value that starts with [ or { will be parsed as a JSON (this is not valid CSS, but is useful for creating extensions).
// Comments that start with /*@ will be parsed; this allows for invalid CSS to be included but pass the validator.

// For example (ignore the newlines in the string):
// $.parsecss('
//   a {
//     font-weight: bold;
//  /*@-jquery-dropshadow: { distance: 5, opacity: 12 }; */ /* a potential use: jquery plugins from the css */
//   }
//
//   div.one , div.two:nth-child(3n+1) {
//     color: green;
//     color-list: [ '#880000', '#008800', '#000088'] /* the parser doesn't care about invalid CSS as long as it's syntactically correct */
//       /* but the JSON extension requires good javascript ([ #880000, #008800, #000088] would fail, without the quotes) */
//   }
//  ');

// would return (in JSON format):
// {
//   'a' : {
//     'font-weight' : 'bold',
//     '-jquery-dropshadow' : {
//       'distance ': 5,
//       'opacity' : 12
//     },
//   'div.one , div.two:nth-child(3n+1)' : {
//     'color' : 'green',
//     'color-list' : [
//       '#880000',
//       '#008800',
//       '#000088'
//     ]
//   }
// }

// It is smart enough to understand media= attributes in <style> and <link> elements, and can parse @import and @media statements.
// It will ignore <link> elements with disabled=true.

// Also exposes some other useful functions:
//   $.parsecss.mediumApplies(str) returns true if a media description (like 'screen', 'print, handheld' or 'all') applies to
//     the current page. Returns true if str == undefined.
//   $.parsecss.parsecsstext(str) returns just the object from the cssText (assumes comments have already been stripped out). Thus
//      $.parsecss.parsecsstext('font-weight: bold; -jquery-dropshadow: { distance: 5, opacity: 12 };') returns
//      { 'font-weight' : 'bold', '-jquery-dropshadow' : {'distance ': 5, 'opacity' : 12 }
//   $.parsecss.isValidSelector(str) returns true if the browser's native CSS can handle the selector. Thus in IE 6,
//     $.parsecss.isValidSelector('div span') returns true and $.parsecss.isValidSelector('div > span') returns false.
//     In FireFox 2, $.parsecss.isValidSelector('div > span') returns true and $.parsecss.isValidSelector('div:nth-child(2)')
//     returns false. Useful for deciding if you want jQuery's CSS command to handle it; note that
//     $(selector).css($.parsecss()[selector]) would do exactly that.

(function($){
  $.parsecss = function(str){
    var parsed = {};
    if (arguments.length == 0){
      // no string; parse all the styles of the document recusively
      $('style , link[type=text/css]').each(function(){
        if (!this.disabled && $.parsecss.mediumApplies(this.media)){
          if (this.href){
            $.ajax({
              url: this.href,
              dataType: 'text',
              success: function(text) {$.extend(parsed, $.parsecss(text)); },
              async: true // I don't like this, but how can I avoid it?
            });
          }else{
            $.extend(parsed, $.parsecss(this.innerHTML));
          }
        }
      });
      return parsed;
    }
    $.each (munge(str).split(/`b%|;/).slice(0,-1), function(i,css){ // split on the end of a block or a semicolon (for @rules)
      if (/^\s*@/.test(css)){
        processAtRule($.trim(css), parsed);
        return;
      }
      css = css.split('%b`'); // css[1] is now the index in munged for the cssText
      var selectors = css[0].split(','); // separate on the the commas
      var declarations = munged['%b`'+css[1]+'`b%'].replace(/(^\s*{\s*)|(\s*}\s*$)/g, ''); // find the string and remove the surrounding braces
      declarations = parsedeclarations(declarations);
      $.each (selectors, function (i, selector){
        selector = restore(selector);
        parsed[selector] = parsed[selector] || {}; // create it if it doesn't exist
        $.extend(parsed[selector], declarations);
      });
    });
    return parsed;
  };

  $.parsecss.parsecsstext = function(str){
    return parsedeclarations(munge(str));
  };

  $.parsecss.mediumApplies = function(str){
    if (!str) return true; // if no descriptor, everything applies
    if (str in media) return media[str];
    var style = newsheet(str);
    if (style.styleSheet){
      // IE
      style.styleSheet.addRule('body', 'z-index: 1');
    }else if (style.sheet){
      // standards. Safari requires position before it accepts z-index
      style.sheet.insertRule('body {position: relative; z-index: 1}', 0);
    }
    media[str] = $('body').css('z-index') == 1;
    $(style).remove();
    return media[str];
  };

  $.parsecss.isValidSelector = function(str){
    str += '{}'; // make a rule out of it
    var style = newsheet();
    if (style.styleSheet){
      // IE freezes up if addRule gets a selector it doesn't understand, but parses cssText fine and turns it to UNKNOWN
      style.styleSheet.cssText = str;
      var ret = !/UNKNOWN/i.test(style.styleSheet.cssText);
    }else if (style.sheet){
      // standards
      try {
        style.sheet.insertRule(str, 0);
        ret = style.sheet.cssRules.length > 0; // the browser accepted it; now see if it stuck (Opera gets here)
      }catch(e) {
        ret = false; // browser couldn't handle it
      }
    }
    $(style).remove();
    return ret;
  };

  // caches
  var media = {}; // media description strings
  var munged = {}; // strings that were removed by the parser so they don't mess up searching for specific characters
  var testDiv; // <div> that we use to test for media

  // private functions

  // create a new stylesheet
  function newsheet(media){
    // Things like this make us crazy:
    // Safari only creates the stylesheet if there is some text in the style element,
    // while Opera crashes if the original statement has any text [as $('<style> </style>') ].
    // IE crashes if we try to append a text node to a style element.
    // The below satisfies all of them
    if (media) media = 'media="'+media+'"';
    var style = $('<style type="text/css"'+media+' />').appendTo('head')[0];
    try {
      style.appendChild(document.createTextNode(''));
    }catch(e){ /* nothing */ }
    return style;
  }

  function parsedeclarations(str){ // assumes the strings have been munged
    var parsed = {};
    $.each (str.split(';'), function (i, decl){
      decl = decl.split(':');
      if (decl.length < 2) return;
      var attr = restore(decl[0]).replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();}); // make it camelCase (animate likes this)
      var value = restore(decl[1]);
      if (value[0] == '{' || value[0] == '[') value = eval('('+value+')'); // evaluate as a JSON
      parsed[attr] = value;
    });
    return parsed;
  }

  // replace strings and brace-surrounded blocks with %s`number`s% and %b`number`b%. By successively taking out the innermost
  // blocks, we ensure that we're matching braces. No way to do this with just regular expressions
  var REstring = /("([^\\\"]|\\.|\\\n)*"|'([^\\\']|\\.|\\\n)*')/;
  var REbraces = /({([^\\}{]|\\.)*})/
  var REmunged = /(%\w`\d+`\w%)/;
  var uid = 0; // unique id number
  function munge(str){
    // remove comments and newlines (replace with space)
    // but first strip /*@ */ comments that should be seen by this parser but ignored by the browser (to let invalid CSS through)
    str = str.replace(/\/\*@(([^\*]|\*[^\/])*)\*\//g,'$1').replace(/\/\*([^\*]|\*[^\/])*\*\/|\n/g,' ');
    // replace strings
    while (match = REstring.exec(str)){
      var replacement = '%s`'+(uid++)+'`s%';
      munged[replacement] = match[1];
      str = str.replace(REstring, replacement);
    }
    while (match = REbraces.exec(str)){
      var replacement = '%b`'+(uid++)+'`b%';
      munged[replacement] = match[1];
      str = str.replace(REbraces, replacement);
    }
    return str;
  }

  function restore(str){
    while (match = REmunged.exec(str)){
      str = str.replace(REmunged, munged[match[1]]);
    }
    return $.trim(str);
  }

  function processAtRule (rule, parsed){
    var split = rule.replace(/\s+/, ' ').split(' '); // remove extra whitespace
    var type = split.shift();
    if (type=='@media'){
      var css = restore(split.pop()+'`b%').replace(/^{|}$/,''); // remove outer braces
      if ($.parsecss.mediumApplies(split.join(' '))){
        $.extend(parsed, $.parsecss(css));
      }
    }else if (type='@import'){
      var url = restore(split.shift());
      if ($.parsecss.mediumApplies(split.join(' '))){
        url = url.replace(/^url\(|\)$/gi, '').replace(/^["']|["']$/, ''); // remove the url('...') wrapper
        $.ajax({
          url: url,
          dataType: 'text',
          success: function(text) { $.extend(parsed, $.parsecss(text)); },
          async: true // I don't like this, but how can I avoid it?
        });
      }
    }
  }
})(jQuery);

