text: /*global Tokens, TokenStream, SyntaxError, Properties, Validation, ValidationError, SyntaxUnit,
      PropertyValue, PropertyValuePart, SelectorPart, SelectorSubPart, Selector,
      PropertyName, Combinator, MediaFeature, MediaQuery, EventTarget */
  
  /**
   * A CSS3 parser.
   * @namespace parserlib.css
   * @class Parser
   * @constructor
   * @param {Object} options (Optional) Various options for the parser:
   *      starHack (true|false) to allow IE6 star hack as valid,
   *      underscoreHack (true|false) to interpret leading underscores
   *      as IE6-7 targeting for known properties, ieFilters (true|false)
   *      to indicate that IE < 8 filters should be accepted and not throw
   *      syntax errors.
   */
  function Parser(options){
  
      //inherit event functionality
      EventTarget.call(this);
  
  
      this.options = options || {};
  
      this._tokenStream = null;
  }
  
  //Static constants
  Parser.DEFAULT_TYPE = 0;
  Parser.COMBINATOR_TYPE = 1;
  Parser.MEDIA_FEATURE_TYPE = 2;
  Parser.MEDIA_QUERY_TYPE = 3;
  Parser.PROPERTY_NAME_TYPE = 4;
  Parser.PROPERTY_VALUE_TYPE = 5;
  Parser.PROPERTY_VALUE_PART_TYPE = 6;
  Parser.SELECTOR_TYPE = 7;
  Parser.SELECTOR_PART_TYPE = 8;
  Parser.SELECTOR_SUB_PART_TYPE = 9;
  
  Parser.prototype = function(){
  
      var proto = new EventTarget(),  //new prototype
          prop,
          additions =  {
  
              //restore constructor
              constructor: Parser,
  
              //instance constants - yuck
              DEFAULT_TYPE : 0,
              COMBINATOR_TYPE : 1,
              MEDIA_FEATURE_TYPE : 2,
              MEDIA_QUERY_TYPE : 3,
              PROPERTY_NAME_TYPE : 4,
              PROPERTY_VALUE_TYPE : 5,
              PROPERTY_VALUE_PART_TYPE : 6,
              SELECTOR_TYPE : 7,
              SELECTOR_PART_TYPE : 8,
              SELECTOR_SUB_PART_TYPE : 9,
  
              //-----------------------------------------------------------------
              // Grammar
              //-----------------------------------------------------------------
  
              _stylesheet: function(){
  
                  /*
                   * stylesheet
                   *  : [ CHARSET_SYM S* STRING S* ';' ]?
                   *    [S|CDO|CDC]* [ import [S|CDO|CDC]* ]*
                   *    [ namespace [S|CDO|CDC]* ]*
                   *    [ [ ruleset | media | page | font_face | keyframes ] [S|CDO|CDC]* ]*
                   *  ;
                   */
  
                  var tokenStream = this._tokenStream,
                      charset     = null,
                      count,
                      token,
                      tt;
  
                  this.fire("startstylesheet");
  
                  //try to read character set
                  this._charset();
  
                  this._skipCruft();
  
                  //try to read imports - may be more than one
                  while (tokenStream.peek() == Tokens.IMPORT_SYM){
                      this._import();
                      this._skipCruft();
                  }
  
                  //try to read namespaces - may be more than one
                  while (tokenStream.peek() == Tokens.NAMESPACE_SYM){
                      this._namespace();
                      this._skipCruft();
                  }
  
                  //get the next token
                  tt = tokenStream.peek();
  
                  //try to read the rest
                  while(tt > Tokens.EOF){
  
                      try {
  
                          switch(tt){
                              case Tokens.MEDIA_SYM:
                                  this._media();
                                  this._skipCruft();
                                  break;
                              case Tokens.PAGE_SYM:
                                  this._page();
                                  this._skipCruft();
                                  break;
                              case Tokens.FONT_FACE_SYM:
                                  this._font_face();
                                  this._skipCruft();
                                  break;
                              case Tokens.KEYFRAMES_SYM:
                                  this._keyframes();
                                  this._skipCruft();
                                  break;
                              case Tokens.VIEWPORT_SYM:
                                  this._viewport();
                                  this._skipCruft();
                                  break;
                              case Tokens.UNKNOWN_SYM:  //unknown @ rule
                                  tokenStream.get();
                                  if (!this.options.strict){
  
                                      //fire error event
                                      this.fire({
                                          type:       "error",
                                          error:      null,
                                          message:    "Unknown @ rule: " + tokenStream.LT(0).value + ".",
                                          line:       tokenStream.LT(0).startLine,
                                          col:        tokenStream.LT(0).startCol
                                      });
  
                                      //skip braces
                                      count=0;
                                      while (tokenStream.advance([Tokens.LBRACE, Tokens.RBRACE]) == Tokens.LBRACE){
                                          count++;    //keep track of nesting depth
                                      }
  
                                      while(count){
                                          tokenStream.advance([Tokens.RBRACE]);
                                          count--;
                                      }
  
                                  } else {
                                      //not a syntax error, rethrow it
                                      throw new SyntaxError("Unknown @ rule.", tokenStream.LT(0).startLine, tokenStream.LT(0).startCol);
                                  }
                                  break;
                              case Tokens.S:
                                  this._readWhitespace();
                                  break;
                              default:
                                  if(!this._ruleset()){
  
                                      //error handling for known issues
                                      switch(tt){
                                          case Tokens.CHARSET_SYM:
                                              token = tokenStream.LT(1);
                                              this._charset(false);
                                              throw new SyntaxError("@charset not allowed here.", token.startLine, token.startCol);
                                          case Tokens.IMPORT_SYM:
                                              token = tokenStream.LT(1);
                                              this._import(false);
                                              throw new SyntaxError("@import not allowed here.", token.startLine, token.startCol);
                                          case Tokens.NAMESPACE_SYM:
                                              token = tokenStream.LT(1);
                                              this._namespace(false);
                                              throw new SyntaxError("@namespace not allowed here.", token.startLine, token.startCol);
                                          default:
                                              tokenStream.get();  //get the last token
                                              this._unexpectedToken(tokenStream.token());
                                      }
  
                                  }
                          }
                      } catch(ex) {
                          if (ex instanceof SyntaxError && !this.options.strict){
                              this.fire({
                                  type:       "error",
                                  error:      ex,
                                  message:    ex.message,
                                  line:       ex.line,
                                  col:        ex.col
                              });
                          } else {
                              throw ex;
                          }
                      }
  
                      tt = tokenStream.peek();
                  }
  
                  if (tt != Tokens.EOF){
                      this._unexpectedToken(tokenStream.token());
                  }
  
                  this.fire("endstylesheet");
              },
  
              _charset: function(emit){
                  var tokenStream = this._tokenStream,
                      charset,
                      token,
                      line,
                      col;
  
                  if (tokenStream.match(Tokens.CHARSET_SYM)){
                      line = tokenStream.token().startLine;
                      col = tokenStream.token().startCol;
  
                      this._readWhitespace();
                      tokenStream.mustMatch(Tokens.STRING);
  
                      token = tokenStream.token();
                      charset = token.value;
  
                      this._readWhitespace();
                      tokenStream.mustMatch(Tokens.SEMICOLON);
  
                      if (emit !== false){
                          this.fire({
                              type:   "charset",
                              charset:charset,
                              line:   line,
                              col:    col
                          });
                      }
                  }
              },
  
              _import: function(emit){
                  /*
                   * import
                   *   : IMPORT_SYM S*
                   *    [STRING|URI] S* media_query_list? ';' S*
                   */
  
                  var tokenStream = this._tokenStream,
                      tt,
                      uri,
                      importToken,
                      mediaList   = [];
  
                  //read import symbol
                  tokenStream.mustMatch(Tokens.IMPORT_SYM);
                  importToken = tokenStream.token();
                  this._readWhitespace();
  
                  tokenStream.mustMatch([Tokens.STRING, Tokens.URI]);
  
                  //grab the URI value
                  uri = tokenStream.token().value.replace(/^(?:url\()?["']?([^"']+?)["']?\)?$/, "$1");
  
                  this._readWhitespace();
  
                  mediaList = this._media_query_list();
  
                  //must end with a semicolon
                  tokenStream.mustMatch(Tokens.SEMICOLON);
                  this._readWhitespace();
  
                  if (emit !== false){
                      this.fire({
                          type:   "import",
                          uri:    uri,
                          media:  mediaList,
                          line:   importToken.startLine,
                          col:    importToken.startCol
                      });
                  }
  
              },
  
              _namespace: function(emit){
                  /*
                   * namespace
                   *   : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S*
                   */
  
                  var tokenStream = this._tokenStream,
                      line,
                      col,
                      prefix,
                      uri;
  
                  //read import symbol
                  tokenStream.mustMatch(Tokens.NAMESPACE_SYM);
                  line = tokenStream.token().startLine;
                  col = tokenStream.token().startCol;
                  this._readWhitespace();
  
                  //it's a namespace prefix - no _namespace_prefix() method because it's just an IDENT
                  if (tokenStream.match(Tokens.IDENT)){
                      prefix = tokenStream.token().value;
                      this._readWhitespace();
                  }
  
                  tokenStream.mustMatch([Tokens.STRING, Tokens.URI]);
                  /*if (!tokenStream.match(Tokens.STRING)){
                      tokenStream.mustMatch(Tokens.URI);
                  }*/
  
                  //grab the URI value
                  uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1");
  
                  this._readWhitespace();
  
                  //must end with a semicolon
                  tokenStream.mustMatch(Tokens.SEMICOLON);
                  this._readWhitespace();
  
                  if (emit !== false){
                      this.fire({
                          type:   "namespace",
                          prefix: prefix,
                          uri:    uri,
                          line:   line,
                          col:    col
                      });
                  }
  
              },
  
              _media: function(){
                  /*
                   * media
                   *   : MEDIA_SYM S* media_query_list S* '{' S* ruleset* '}' S*
                   *   ;
                   */
                  var tokenStream     = this._tokenStream,
                      line,
                      col,
                      mediaList;//       = [];
  
                  //look for @media
                  tokenStream.mustMatch(Tokens.MEDIA_SYM);
                  line = tokenStream.token().startLine;
                  col = tokenStream.token().startCol;
  
                  this._readWhitespace();
  
                  mediaList = this._media_query_list();
  
                  tokenStream.mustMatch(Tokens.LBRACE);
                  this._readWhitespace();
  
                  this.fire({
                      type:   "startmedia",
                      media:  mediaList,
                      line:   line,
                      col:    col
                  });
  
                  while(true) {
                      if (tokenStream.peek() == Tokens.PAGE_SYM){
                          this._page();
                      } else if (tokenStream.peek() == Tokens.FONT_FACE_SYM){
                          this._font_face();
                      } else if (tokenStream.peek() == Tokens.VIEWPORT_SYM){
                          this._viewport();
                      } else if (!this._ruleset()){
                          break;
                      }
                  }
  
                  tokenStream.mustMatch(Tokens.RBRACE);
                  this._readWhitespace();
  
                  this.fire({
                      type:   "endmedia",
                      media:  mediaList,
                      line:   line,
                      col:    col
                  });
              },
  
  
              //CSS3 Media Queries
              _media_query_list: function(){
                  /*
                   * media_query_list
                   *   : S* [media_query [ ',' S* media_query ]* ]?
                   *   ;
                   */
                  var tokenStream = this._tokenStream,
                      mediaList   = [];
  
  
                  this._readWhitespace();
  
                  if (tokenStream.peek() == Tokens.IDENT || tokenStream.peek() == Tokens.LPAREN){
                      mediaList.push(this._media_query());
                  }
  
                  while(tokenStream.match(Tokens.COMMA)){
                      this._readWhitespace();
                      mediaList.push(this._media_query());
                  }
  
                  return mediaList;
              },
  
              /*
               * Note: "expression" in the grammar maps to the _media_expression
               * method.
  
               */
              _media_query: function(){
                  /*
                   * media_query
                   *   : [ONLY | NOT]? S* media_type S* [ AND S* expression ]*
                   *   | expression [ AND S* expression ]*
                   *   ;
                   */
                  var tokenStream = this._tokenStream,
                      type        = null,
                      ident       = null,
                      token       = null,
                      expressions = [];
  
                  if (tokenStream.match(Tokens.IDENT)){
                      ident = tokenStream.token().value.toLowerCase();
  
                      //since there's no custom tokens for these, need to manually check
                      if (ident != "only" && ident != "not"){
                          tokenStream.unget();
                          ident = null;
                      } else {
                          token = tokenStream.token();
                      }
                  }
  
                  this._readWhitespace();
  
                  if (tokenStream.peek() == Tokens.IDENT){
                      type = this._media_type();
                      if (token === null){
                          token = tokenStream.token();
                      }
                  } else if (tokenStream.peek() == Tokens.LPAREN){
                      if (token === null){
                          token = tokenStream.LT(1);
                      }
                      expressions.push(this._media_expression());
                  }
  
                  if (type === null && expressions.length === 0){
                      return null;
                  } else {
                      this._readWhitespace();
                      while (tokenStream.match(Tokens.IDENT)){
                          if (tokenStream.token().value.toLowerCase() != "and"){
                              this._unexpectedToken(tokenStream.token());
                          }
  
                          this._readWhitespace();
                          expressions.push(this._media_expression());
                      }
                  }
  
                  return new MediaQuery(ident, type, expressions, token.startLine, token.startCol);
              },
  
              //CSS3 Media Queries
              _media_type: function(){
                  /*
                   * media_type
                   *   : IDENT
                   *   ;
                   */
                  return this._media_feature();
              },
  
              /**
               * Note: in CSS3 Media Queries, this is called "expression".
               * Renamed here to avoid conflict with CSS3 Selectors
               * definition of "expression". Also note that "expr" in the
               * grammar now maps to "expression" from CSS3 selectors.
               * @method _media_expression
               * @private
               */
              _media_expression: function(){
                  /*
                   * expression
                   *  : '(' S* media_feature S* [ ':' S* expr ]? ')' S*
                   *  ;
                   */
                  var tokenStream = this._tokenStream,
                      feature     = null,
                      token,
                      expression  = null;
  
                  tokenStream.mustMatch(Tokens.LPAREN);
  
                  feature = this._media_feature();
                  this._readWhitespace();
  
                  if (tokenStream.match(Tokens.COLON)){
                      this._readWhitespace();
                      token = tokenStream.LT(1);
                      expression = this._expression();
                  }
  
                  tokenStream.mustMatch(Tokens.RPAREN);
                  this._readWhitespace();
  
                  return new MediaFeature(feature, (expression ? new SyntaxUnit(expression, token.startLine, token.startCol) : null));
              },
  
              //CSS3 Media Queries
              _media_feature: function(){
                  /*
                   * media_feature
                   *   : IDENT
                   *   ;
                   */
                  var tokenStream = this._tokenStream;
  
                  tokenStream.mustMatch(Tokens.IDENT);
  
                  return SyntaxUnit.fromToken(tokenStream.token());
              },
  
              //CSS3 Paged Media
              _page: function(){
                  /*
                   * page:
                   *    PAGE_SYM S* IDENT? pseudo_page? S*
                   *    '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S*
                   *    ;
                   */
                  var tokenStream = this._tokenStream,
                      line,
                      col,
                      identifier  = null,
                      pseudoPage  = null;
  
                  //look for @page
                  tokenStream.mustMatch(Tokens.PAGE_SYM);
                  line = tokenStream.token().startLine;
                  col = tokenStream.token().startCol;
  
                  this._readWhitespace();
  
                  if (tokenStream.match(Tokens.IDENT)){
                      identifier = tokenStream.token().value;
  
                      //The value 'auto' may not be used as a page name and MUST be treated as a syntax error.
                      if (identifier.toLowerCase() === "auto"){
                          this._unexpectedToken(tokenStream.token());
                      }
                  }
  
                  //see if there's a colon upcoming
                  if (tokenStream.peek() == Tokens.COLON){
                      pseudoPage = this._pseudo_page();
                  }
  
                  this._readWhitespace();
  
                  this.fire({
                      type:   "startpage",
                      id:     identifier,
                      pseudo: pseudoPage,
                      line:   line,
                      col:    col
                  });
  
                  this._readDeclarations(true, true);
  
                  this.fire({
                      type:   "endpage",
                      id:     identifier,
                      pseudo: pseudoPage,
                      line:   line,
                      col:    col
                  });
  
              },
  
              //CSS3 Paged Media
              _margin: function(){
                  /*
                   * margin :
                   *    margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
                   *    ;
                   */
                  var tokenStream = this._tokenStream,
                      line,
                      col,
                      marginSym   = this._margin_sym();
  
                  if (marginSym){
                      line = tokenStream.token().startLine;
                      col = tokenStream.token().startCol;
  
                      this.fire({
                          type: "startpagemargin",
                          margin: marginSym,
                          line:   line,
                          col:    col
                      });
  
                      this._readDeclarations(true);
  
                      this.fire({
                          type: "endpagemargin",
                          margin: marginSym,
                          line:   line,
                          col:    col
                      });
                      return true;
                  } else {
                      return false;
                  }
              },
  
              //CSS3 Paged Media
              _margin_sym: function(){
  
                  /*
                   * margin_sym :
                   *    TOPLEFTCORNER_SYM |
                   *    TOPLEFT_SYM |
                   *    TOPCENTER_SYM |
                   *    TOPRIGHT_SYM |
                   *    TOPRIGHTCORNER_SYM |
                   *    BOTTOMLEFTCORNER_SYM |
                   *    BOTTOMLEFT_SYM |
                   *    BOTTOMCENTER_SYM |
                   *    BOTTOMRIGHT_SYM |
                   *    BOTTOMRIGHTCORNER_SYM |
                   *    LEFTTOP_SYM |
                   *    LEFTMIDDLE_SYM |
                   *    LEFTBOTTOM_SYM |
                   *    RIGHTTOP_SYM |
                   *    RIGHTMIDDLE_SYM |
                   *    RIGHTBOTTOM_SYM
                   *    ;
                   */
  
                  var tokenStream = this._tokenStream;
  
                  if(tokenStream.match([Tokens.TOPLEFTCORNER_SYM, Tokens.TOPLEFT_SYM,
                          Tokens.TOPCENTER_SYM, Tokens.TOPRIGHT_SYM, Tokens.TOPRIGHTCORNER_SYM,
                          Tokens.BOTTOMLEFTCORNER_SYM, Tokens.BOTTOMLEFT_SYM,
                          Tokens.BOTTOMCENTER_SYM, Tokens.BOTTOMRIGHT_SYM,
                          Tokens.BOTTOMRIGHTCORNER_SYM, Tokens.LEFTTOP_SYM,
                          Tokens.LEFTMIDDLE_SYM, Tokens.LEFTBOTTOM_SYM, Tokens.RIGHTTOP_SYM,
                          Tokens.RIGHTMIDDLE_SYM, Tokens.RIGHTBOTTOM_SYM]))
                  {
                      return SyntaxUnit.fromToken(tokenStream.token());
                  } else {
                      return null;
                  }
  
              },
  
              _pseudo_page: function(){
                  /*
                   * pseudo_page
                   *   : ':' IDENT
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream;
  
                  tokenStream.mustMatch(Tokens.COLON);
                  tokenStream.mustMatch(Tokens.IDENT);
  
                  //TODO: CSS3 Paged Media says only "left", "center", and "right" are allowed
  
                  return tokenStream.token().value;
              },
  
              _font_face: function(){
                  /*
                   * font_face
                   *   : FONT_FACE_SYM S*
                   *     '{' S* declaration [ ';' S* declaration ]* '}' S*
                   *   ;
                   */
                  var tokenStream = this._tokenStream,
                      line,
                      col;
  
                  //look for @page
                  tokenStream.mustMatch(Tokens.FONT_FACE_SYM);
                  line = tokenStream.token().startLine;
                  col = tokenStream.token().startCol;
  
                  this._readWhitespace();
  
                  this.fire({
                      type:   "startfontface",
                      line:   line,
                      col:    col
                  });
  
                  this._readDeclarations(true);
  
                  this.fire({
                      type:   "endfontface",
                      line:   line,
                      col:    col
                  });
              },
  
              _viewport: function(){
                  /*
                   * viewport
                   *   : VIEWPORT_SYM S*
                   *     '{' S* declaration? [ ';' S* declaration? ]* '}' S*
                   *   ;
                   */
                   var tokenStream = this._tokenStream,
                      line,
                      col;
  
                      tokenStream.mustMatch(Tokens.VIEWPORT_SYM);
                      line = tokenStream.token().startLine;
                      col = tokenStream.token().startCol;
  
                      this._readWhitespace();
  
                      this.fire({
                          type:   "startviewport",
                          line:   line,
                          col:    col
                      });
  
                      this._readDeclarations(true);
  
                      this.fire({
                          type:   "endviewport",
                          line:   line,
                          col:    col
                      });
  
              },
  
              _operator: function(inFunction){
  
                  /*
                   * operator (outside function)
                   *  : '/' S* | ',' S* | /( empty )/
                   * operator (inside function)
                   *  : '/' S* | '+' S* | '*' S* | '-' S* /( empty )/
                   *  ;
                   */
  
                  var tokenStream = this._tokenStream,
                      token       = null;
  
                  if (tokenStream.match([Tokens.SLASH, Tokens.COMMA]) ||
                      (inFunction && tokenStream.match([Tokens.PLUS, Tokens.STAR, Tokens.MINUS]))){
                      token =  tokenStream.token();
                      this._readWhitespace();
                  }
                  return token ? PropertyValuePart.fromToken(token) : null;
  
              },
  
              _combinator: function(){
  
                  /*
                   * combinator
                   *  : PLUS S* | GREATER S* | TILDE S* | S+
                   *  ;
                   */
  
                  var tokenStream = this._tokenStream,
                      value       = null,
                      token;
  
                  if(tokenStream.match([Tokens.PLUS, Tokens.GREATER, Tokens.TILDE])){
                      token = tokenStream.token();
                      value = new Combinator(token.value, token.startLine, token.startCol);
                      this._readWhitespace();
                  }
  
                  return value;
              },
  
              _unary_operator: function(){
  
                  /*
                   * unary_operator
                   *  : '-' | '+'
                   *  ;
                   */
  
                  var tokenStream = this._tokenStream;
  
                  if (tokenStream.match([Tokens.MINUS, Tokens.PLUS])){
                      return tokenStream.token().value;
                  } else {
                      return null;
                  }
              },
  
              _property: function(){
  
                  /*
                   * property
                   *   : IDENT S*
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      value       = null,
                      hack        = null,
                      tokenValue,
                      token,
                      line,
                      col;
  
                  //check for star hack - throws error if not allowed
                  if (tokenStream.peek() == Tokens.STAR && this.options.starHack){
                      tokenStream.get();
                      token = tokenStream.token();
                      hack = token.value;
                      line = token.startLine;
                      col = token.startCol;
                  }
  
                  if(tokenStream.match(Tokens.IDENT)){
                      token = tokenStream.token();
                      tokenValue = token.value;
  
                      //check for underscore hack - no error if not allowed because it's valid CSS syntax
                      if (tokenValue.charAt(0) == "_" && this.options.underscoreHack){
                          hack = "_";
                          tokenValue = tokenValue.substring(1);
                      }
  
                      value = new PropertyName(tokenValue, hack, (line||token.startLine), (col||token.startCol));
                      this._readWhitespace();
                  }
  
                  return value;
              },
  
              //Augmented with CSS3 Selectors
              _ruleset: function(){
                  /*
                   * ruleset
                   *   : selectors_group
                   *     '{' S* declaration? [ ';' S* declaration? ]* '}' S*
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      tt,
                      selectors;
  
  
                  /*
                   * Error Recovery: If even a single selector fails to parse,
                   * then the entire ruleset should be thrown away.
                   */
                  try {
                      selectors = this._selectors_group();
                  } catch (ex){
                      if (ex instanceof SyntaxError && !this.options.strict){
  
                          //fire error event
                          this.fire({
                              type:       "error",
                              error:      ex,
                              message:    ex.message,
                              line:       ex.line,
                              col:        ex.col
                          });
  
                          //skip over everything until closing brace
                          tt = tokenStream.advance([Tokens.RBRACE]);
                          if (tt == Tokens.RBRACE){
                              //if there's a right brace, the rule is finished so don't do anything
                          } else {
                              //otherwise, rethrow the error because it wasn't handled properly
                              throw ex;
                          }
  
                      } else {
                          //not a syntax error, rethrow it
                          throw ex;
                      }
  
                      //trigger parser to continue
                      return true;
                  }
  
                  //if it got here, all selectors parsed
                  if (selectors){
  
                      this.fire({
                          type:       "startrule",
                          selectors:  selectors,
                          line:       selectors[0].line,
                          col:        selectors[0].col
                      });
  
                      this._readDeclarations(true);
  
                      this.fire({
                          type:       "endrule",
                          selectors:  selectors,
                          line:       selectors[0].line,
                          col:        selectors[0].col
                      });
  
                  }
  
                  return selectors;
  
              },
  
              //CSS3 Selectors
              _selectors_group: function(){
  
                  /*
                   * selectors_group
                   *   : selector [ COMMA S* selector ]*
                   *   ;
                   */
                  var tokenStream = this._tokenStream,
                      selectors   = [],
                      selector;
  
                  selector = this._selector();
                  if (selector !== null){
  
                      selectors.push(selector);
                      while(tokenStream.match(Tokens.COMMA)){
                          this._readWhitespace();
                          selector = this._selector();
                          if (selector !== null){
                              selectors.push(selector);
                          } else {
                              this._unexpectedToken(tokenStream.LT(1));
                          }
                      }
                  }
  
                  return selectors.length ? selectors : null;
              },
  
              //CSS3 Selectors
              _selector: function(){
                  /*
                   * selector
                   *   : simple_selector_sequence [ combinator simple_selector_sequence ]*
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      selector    = [],
                      nextSelector = null,
                      combinator  = null,
                      ws          = null;
  
                  //if there's no simple selector, then there's no selector
                  nextSelector = this._simple_selector_sequence();
                  if (nextSelector === null){
                      return null;
                  }
  
                  selector.push(nextSelector);
  
                  do {
  
                      //look for a combinator
                      combinator = this._combinator();
  
                      if (combinator !== null){
                          selector.push(combinator);
                          nextSelector = this._simple_selector_sequence();
  
                          //there must be a next selector
                          if (nextSelector === null){
                              this._unexpectedToken(tokenStream.LT(1));
                          } else {
  
                              //nextSelector is an instance of SelectorPart
                              selector.push(nextSelector);
                          }
                      } else {
  
                          //if there's not whitespace, we're done
                          if (this._readWhitespace()){
  
                              //add whitespace separator
                              ws = new Combinator(tokenStream.token().value, tokenStream.token().startLine, tokenStream.token().startCol);
  
                              //combinator is not required
                              combinator = this._combinator();
  
                              //selector is required if there's a combinator
                              nextSelector = this._simple_selector_sequence();
                              if (nextSelector === null){
                                  if (combinator !== null){
                                      this._unexpectedToken(tokenStream.LT(1));
                                  }
                              } else {
  
                                  if (combinator !== null){
                                      selector.push(combinator);
                                  } else {
                                      selector.push(ws);
                                  }
  
                                  selector.push(nextSelector);
                              }
                          } else {
                              break;
                          }
  
                      }
                  } while(true);
  
                  return new Selector(selector, selector[0].line, selector[0].col);
              },
  
              //CSS3 Selectors
              _simple_selector_sequence: function(){
                  /*
                   * simple_selector_sequence
                   *   : [ type_selector | universal ]
                   *     [ HASH | class | attrib | pseudo | negation ]*
                   *   | [ HASH | class | attrib | pseudo | negation ]+
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
  
                      //parts of a simple selector
                      elementName = null,
                      modifiers   = [],
  
                      //complete selector text
                      selectorText= "",
  
                      //the different parts after the element name to search for
                      components  = [
                          //HASH
                          function(){
                              return tokenStream.match(Tokens.HASH) ?
                                      new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) :
                                      null;
                          },
                          this._class,
                          this._attrib,
                          this._pseudo,
                          this._negation
                      ],
                      i           = 0,
                      len         = components.length,
                      component   = null,
                      found       = false,
                      line,
                      col;
  
  
                  //get starting line and column for the selector
                  line = tokenStream.LT(1).startLine;
                  col = tokenStream.LT(1).startCol;
  
                  elementName = this._type_selector();
                  if (!elementName){
                      elementName = this._universal();
                  }
  
                  if (elementName !== null){
                      selectorText += elementName;
                  }
  
                  while(true){
  
                      //whitespace means we're done
                      if (tokenStream.peek() === Tokens.S){
                          break;
                      }
  
                      //check for each component
                      while(i < len && component === null){
                          component = components[i++].call(this);
                      }
  
                      if (component === null){
  
                          //we don't have a selector
                          if (selectorText === ""){
                              return null;
                          } else {
                              break;
                          }
                      } else {
                          i = 0;
                          modifiers.push(component);
                          selectorText += component.toString();
                          component = null;
                      }
                  }
  
  
                  return selectorText !== "" ?
                          new SelectorPart(elementName, modifiers, selectorText, line, col) :
                          null;
              },
  
              //CSS3 Selectors
              _type_selector: function(){
                  /*
                   * type_selector
                   *   : [ namespace_prefix ]? element_name
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      ns          = this._namespace_prefix(),
                      elementName = this._element_name();
  
                  if (!elementName){
                      /*
                       * Need to back out the namespace that was read due to both
                       * type_selector and universal reading namespace_prefix
                       * first. Kind of hacky, but only way I can figure out
                       * right now how to not change the grammar.
                       */
                      if (ns){
                          tokenStream.unget();
                          if (ns.length > 1){
                              tokenStream.unget();
                          }
                      }
  
                      return null;
                  } else {
                      if (ns){
                          elementName.text = ns + elementName.text;
                          elementName.col -= ns.length;
                      }
                      return elementName;
                  }
              },
  
              //CSS3 Selectors
              _class: function(){
                  /*
                   * class
                   *   : '.' IDENT
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      token;
  
                  if (tokenStream.match(Tokens.DOT)){
                      tokenStream.mustMatch(Tokens.IDENT);
                      token = tokenStream.token();
                      return new SelectorSubPart("." + token.value, "class", token.startLine, token.startCol - 1);
                  } else {
                      return null;
                  }
  
              },
  
              //CSS3 Selectors
              _element_name: function(){
                  /*
                   * element_name
                   *   : IDENT
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      token;
  
                  if (tokenStream.match(Tokens.IDENT)){
                      token = tokenStream.token();
                      return new SelectorSubPart(token.value, "elementName", token.startLine, token.startCol);
  
                  } else {
                      return null;
                  }
              },
  
              //CSS3 Selectors
              _namespace_prefix: function(){
                  /*
                   * namespace_prefix
                   *   : [ IDENT | '*' ]? '|'
                   *   ;
                   */
                  var tokenStream = this._tokenStream,
                      value       = "";
  
                  //verify that this is a namespace prefix
                  if (tokenStream.LA(1) === Tokens.PIPE || tokenStream.LA(2) === Tokens.PIPE){
  
                      if(tokenStream.match([Tokens.IDENT, Tokens.STAR])){
                          value += tokenStream.token().value;
                      }
  
                      tokenStream.mustMatch(Tokens.PIPE);
                      value += "|";
  
                  }
  
                  return value.length ? value : null;
              },
  
              //CSS3 Selectors
              _universal: function(){
                  /*
                   * universal
                   *   : [ namespace_prefix ]? '*'
                   *   ;
                   */
                  var tokenStream = this._tokenStream,
                      value       = "",
                      ns;
  
                  ns = this._namespace_prefix();
                  if(ns){
                      value += ns;
                  }
  
                  if(tokenStream.match(Tokens.STAR)){
                      value += "*";
                  }
  
                  return value.length ? value : null;
  
             },
  
              //CSS3 Selectors
              _attrib: function(){
                  /*
                   * attrib
                   *   : '[' S* [ namespace_prefix ]? IDENT S*
                   *         [ [ PREFIXMATCH |
                   *             SUFFIXMATCH |
                   *             SUBSTRINGMATCH |
                   *             '=' |
                   *             INCLUDES |
                   *             DASHMATCH ] S* [ IDENT | STRING ] S*
                   *         ]? ']'
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      value       = null,
                      ns,
                      token;
  
                  if (tokenStream.match(Tokens.LBRACKET)){
                      token = tokenStream.token();
                      value = token.value;
                      value += this._readWhitespace();
  
                      ns = this._namespace_prefix();
  
                      if (ns){
                          value += ns;
                      }
  
                      tokenStream.mustMatch(Tokens.IDENT);
                      value += tokenStream.token().value;
                      value += this._readWhitespace();
  
                      if(tokenStream.match([Tokens.PREFIXMATCH, Tokens.SUFFIXMATCH, Tokens.SUBSTRINGMATCH,
                              Tokens.EQUALS, Tokens.INCLUDES, Tokens.DASHMATCH])){
  
                          value += tokenStream.token().value;
                          value += this._readWhitespace();
  
                          tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]);
                          value += tokenStream.token().value;
                          value += this._readWhitespace();
                      }
  
                      tokenStream.mustMatch(Tokens.RBRACKET);
  
                      return new SelectorSubPart(value + "]", "attribute", token.startLine, token.startCol);
                  } else {
                      return null;
                  }
              },
  
              //CSS3 Selectors
              _pseudo: function(){
  
                  /*
                   * pseudo
                   *   : ':' ':'? [ IDENT | functional_pseudo ]
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      pseudo      = null,
                      colons      = ":",
                      line,
                      col;
  
                  if (tokenStream.match(Tokens.COLON)){
  
                      if (tokenStream.match(Tokens.COLON)){
                          colons += ":";
                      }
  
                      if (tokenStream.match(Tokens.IDENT)){
                          pseudo = tokenStream.token().value;
                          line = tokenStream.token().startLine;
                          col = tokenStream.token().startCol - colons.length;
                      } else if (tokenStream.peek() == Tokens.FUNCTION){
                          line = tokenStream.LT(1).startLine;
                          col = tokenStream.LT(1).startCol - colons.length;
                          pseudo = this._functional_pseudo();
                      }
  
                      if (pseudo){
                          pseudo = new SelectorSubPart(colons + pseudo, "pseudo", line, col);
                      }
                  }
  
                  return pseudo;
              },
  
              //CSS3 Selectors
              _functional_pseudo: function(){
                  /*
                   * functional_pseudo
                   *   : FUNCTION S* expression ')'
                   *   ;
                  */
  
                  var tokenStream = this._tokenStream,
                      value = null;
  
                  if(tokenStream.match(Tokens.FUNCTION)){
                      value = tokenStream.token().value;
                      value += this._readWhitespace();
                      value += this._expression();
                      tokenStream.mustMatch(Tokens.RPAREN);
                      value += ")";
                  }
  
                  return value;
              },
  
              //CSS3 Selectors
              _expression: function(){
                  /*
                   * expression
                   *   : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      value       = "";
  
                  while(tokenStream.match([Tokens.PLUS, Tokens.MINUS, Tokens.DIMENSION,
                          Tokens.NUMBER, Tokens.STRING, Tokens.IDENT, Tokens.LENGTH,
                          Tokens.FREQ, Tokens.ANGLE, Tokens.TIME,
                          Tokens.RESOLUTION, Tokens.SLASH])){
  
                      value += tokenStream.token().value;
                      value += this._readWhitespace();
                  }
  
                  return value.length ? value : null;
  
              },
  
              //CSS3 Selectors
              _negation: function(){
                  /*
                   * negation
                   *   : NOT S* negation_arg S* ')'
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      line,
                      col,
                      value       = "",
                      arg,
                      subpart     = null;
  
                  if (tokenStream.match(Tokens.NOT)){
                      value = tokenStream.token().value;
                      line = tokenStream.token().startLine;
                      col = tokenStream.token().startCol;
                      value += this._readWhitespace();
                      arg = this._negation_arg();
                      value += arg;
                      value += this._readWhitespace();
                      tokenStream.match(Tokens.RPAREN);
                      value += tokenStream.token().value;
  
                      subpart = new SelectorSubPart(value, "not", line, col);
                      subpart.args.push(arg);
                  }
  
                  return subpart;
              },
  
              //CSS3 Selectors
              _negation_arg: function(){
                  /*
                   * negation_arg
                   *   : type_selector | universal | HASH | class | attrib | pseudo
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      args        = [
                          this._type_selector,
                          this._universal,
                          function(){
                              return tokenStream.match(Tokens.HASH) ?
                                      new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) :
                                      null;
                          },
                          this._class,
                          this._attrib,
                          this._pseudo
                      ],
                      arg         = null,
                      i           = 0,
                      len         = args.length,
                      elementName,
                      line,
                      col,
                      part;
  
                  line = tokenStream.LT(1).startLine;
                  col = tokenStream.LT(1).startCol;
  
                  while(i < len && arg === null){
  
                      arg = args[i].call(this);
                      i++;
                  }
  
                  //must be a negation arg
                  if (arg === null){
                      this._unexpectedToken(tokenStream.LT(1));
                  }
  
                  //it's an element name
                  if (arg.type == "elementName"){
                      part = new SelectorPart(arg, [], arg.toString(), line, col);
                  } else {
                      part = new SelectorPart(null, [arg], arg.toString(), line, col);
                  }
  
                  return part;
              },
  
              _declaration: function(){
  
                  /*
                   * declaration
                   *   : property ':' S* expr prio?
                   *   | /( empty )/
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      property    = null,
                      expr        = null,
                      prio        = null,
                      error       = null,
                      invalid     = null,
                      propertyName= "";
  
                  property = this._property();
                  if (property !== null){
  
                      tokenStream.mustMatch(Tokens.COLON);
                      this._readWhitespace();
  
                      expr = this._expr();
  
                      //if there's no parts for the value, it's an error
                      if (!expr || expr.length === 0){
                          this._unexpectedToken(tokenStream.LT(1));
                      }
  
                      prio = this._prio();
  
                      /*
                       * If hacks should be allowed, then only check the root
                       * property. If hacks should not be allowed, treat
                       * _property or *property as invalid properties.
                       */
                      propertyName = property.toString();
                      if (this.options.starHack && property.hack == "*" ||
                              this.options.underscoreHack && property.hack == "_") {
  
                          propertyName = property.text;
                      }
  
                      try {
                          this._validateProperty(propertyName, expr);
                      } catch (ex) {
                          invalid = ex;
                      }
  
                      this.fire({
                          type:       "property",
                          property:   property,
                          value:      expr,
                          important:  prio,
                          line:       property.line,
                          col:        property.col,
                          invalid:    invalid
                      });
  
                      return true;
                  } else {
                      return false;
                  }
              },
  
              _prio: function(){
                  /*
                   * prio
                   *   : IMPORTANT_SYM S*
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      result      = tokenStream.match(Tokens.IMPORTANT_SYM);
  
                  this._readWhitespace();
                  return result;
              },
  
              _expr: function(inFunction){
                  /*
                   * expr
                   *   : term [ operator term ]*
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      values      = [],
  					//valueParts	= [],
                      value       = null,
                      operator    = null;
  
                  value = this._term(inFunction);
                  if (value !== null){
  
                      values.push(value);
  
                      do {
                          operator = this._operator(inFunction);
  
                          //if there's an operator, keep building up the value parts
                          if (operator){
                              values.push(operator);
                          } /*else {
                              //if there's not an operator, you have a full value
  							values.push(new PropertyValue(valueParts, valueParts[0].line, valueParts[0].col));
  							valueParts = [];
  						}*/
  
                          value = this._term(inFunction);
  
                          if (value === null){
                              break;
                          } else {
                              values.push(value);
                          }
                      } while(true);
                  }
  
  				//cleanup
                  /*if (valueParts.length){
                      values.push(new PropertyValue(valueParts, valueParts[0].line, valueParts[0].col));
                  }*/
  
                  return values.length > 0 ? new PropertyValue(values, values[0].line, values[0].col) : null;
              },
  
              _term: function(inFunction){
  
                  /*
                   * term
                   *   : unary_operator?
                   *     [ NUMBER S* | PERCENTAGE S* | LENGTH S* | ANGLE S* |
                   *       TIME S* | FREQ S* | function | ie_function ]
                   *   | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      unary       = null,
                      value       = null,
                      endChar     = null,
                      token,
                      line,
                      col;
  
                  //returns the operator or null
                  unary = this._unary_operator();
                  if (unary !== null){
                      line = tokenStream.token().startLine;
                      col = tokenStream.token().startCol;
                  }
  
                  //exception for IE filters
                  if (tokenStream.peek() == Tokens.IE_FUNCTION && this.options.ieFilters){
  
                      value = this._ie_function();
                      if (unary === null){
                          line = tokenStream.token().startLine;
                          col = tokenStream.token().startCol;
                      }
  
                  //see if it's a simple block
                  } else if (inFunction && tokenStream.match([Tokens.LPAREN, Tokens.LBRACE, Tokens.LBRACKET])){
  
                      token = tokenStream.token();
                      endChar = token.endChar;
                      value = token.value + this._expr(inFunction).text;
                      if (unary === null){
                          line = tokenStream.token().startLine;
                          col = tokenStream.token().startCol;
                      }
                      tokenStream.mustMatch(Tokens.type(endChar));
                      value += endChar;
                      this._readWhitespace();
  
                  //see if there's a simple match
                  } else if (tokenStream.match([Tokens.NUMBER, Tokens.PERCENTAGE, Tokens.LENGTH,
                          Tokens.ANGLE, Tokens.TIME,
                          Tokens.FREQ, Tokens.STRING, Tokens.IDENT, Tokens.URI, Tokens.UNICODE_RANGE])){
  
                      value = tokenStream.token().value;
                      if (unary === null){
                          line = tokenStream.token().startLine;
                          col = tokenStream.token().startCol;
                      }
                      this._readWhitespace();
                  } else {
  
                      //see if it's a color
                      token = this._hexcolor();
                      if (token === null){
  
                          //if there's no unary, get the start of the next token for line/col info
                          if (unary === null){
                              line = tokenStream.LT(1).startLine;
                              col = tokenStream.LT(1).startCol;
                          }
  
                          //has to be a function
                          if (value === null){
  
                              /*
                               * This checks for alpha(opacity=0) style of IE
                               * functions. IE_FUNCTION only presents progid: style.
                               */
                              if (tokenStream.LA(3) == Tokens.EQUALS && this.options.ieFilters){
                                  value = this._ie_function();
                              } else {
                                  value = this._function();
                              }
                          }
  
                          /*if (value === null){
                              return null;
                              //throw new Error("Expected identifier at line " + tokenStream.token().startLine + ", character " +  tokenStream.token().startCol + ".");
                          }*/
  
                      } else {
                          value = token.value;
                          if (unary === null){
                              line = token.startLine;
                              col = token.startCol;
                          }
                      }
  
                  }
  
                  return value !== null ?
                          new PropertyValuePart(unary !== null ? unary + value : value, line, col) :
                          null;
  
              },
  
              _function: function(){
  
                  /*
                   * function
                   *   : FUNCTION S* expr ')' S*
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      functionText = null,
                      expr        = null,
                      lt;
  
                  if (tokenStream.match(Tokens.FUNCTION)){
                      functionText = tokenStream.token().value;
                      this._readWhitespace();
                      expr = this._expr(true);
                      functionText += expr;
  
                      //START: Horrible hack in case it's an IE filter
                      if (this.options.ieFilters && tokenStream.peek() == Tokens.EQUALS){
                          do {
  
                              if (this._readWhitespace()){
                                  functionText += tokenStream.token().value;
                              }
  
                              //might be second time in the loop
                              if (tokenStream.LA(0) == Tokens.COMMA){
                                  functionText += tokenStream.token().value;
                              }
  
                              tokenStream.match(Tokens.IDENT);
                              functionText += tokenStream.token().value;
  
                              tokenStream.match(Tokens.EQUALS);
                              functionText += tokenStream.token().value;
  
                              //functionText += this._term();
                              lt = tokenStream.peek();
                              while(lt != Tokens.COMMA && lt != Tokens.S && lt != Tokens.RPAREN){
                                  tokenStream.get();
                                  functionText += tokenStream.token().value;
                                  lt = tokenStream.peek();
                              }
                          } while(tokenStream.match([Tokens.COMMA, Tokens.S]));
                      }
  
                      //END: Horrible Hack
  
                      tokenStream.match(Tokens.RPAREN);
                      functionText += ")";
                      this._readWhitespace();
                  }
  
                  return functionText;
              },
  
              _ie_function: function(){
  
                  /* (My own extension)
                   * ie_function
                   *   : IE_FUNCTION S* IDENT '=' term [S* ','? IDENT '=' term]+ ')' S*
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      functionText = null,
                      expr        = null,
                      lt;
  
                  //IE function can begin like a regular function, too
                  if (tokenStream.match([Tokens.IE_FUNCTION, Tokens.FUNCTION])){
                      functionText = tokenStream.token().value;
  
                      do {
  
                          if (this._readWhitespace()){
                              functionText += tokenStream.token().value;
                          }
  
                          //might be second time in the loop
                          if (tokenStream.LA(0) == Tokens.COMMA){
                              functionText += tokenStream.token().value;
                          }
  
                          tokenStream.match(Tokens.IDENT);
                          functionText += tokenStream.token().value;
  
                          tokenStream.match(Tokens.EQUALS);
                          functionText += tokenStream.token().value;
  
                          //functionText += this._term();
                          lt = tokenStream.peek();
                          while(lt != Tokens.COMMA && lt != Tokens.S && lt != Tokens.RPAREN){
                              tokenStream.get();
                              functionText += tokenStream.token().value;
                              lt = tokenStream.peek();
                          }
                      } while(tokenStream.match([Tokens.COMMA, Tokens.S]));
  
                      tokenStream.match(Tokens.RPAREN);
                      functionText += ")";
                      this._readWhitespace();
                  }
  
                  return functionText;
              },
  
              _hexcolor: function(){
                  /*
                   * There is a constraint on the color that it must
                   * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
                   * after the "#"; e.g., "#000" is OK, but "#abcd" is not.
                   *
                   * hexcolor
                   *   : HASH S*
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      token = null,
                      color;
  
                  if(tokenStream.match(Tokens.HASH)){
  
                      //need to do some validation here
  
                      token = tokenStream.token();
                      color = token.value;
                      if (!/#[a-f0-9]{3,6}/i.test(color)){
                          throw new SyntaxError("Expected a hex color but found '" + color + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol);
                      }
                      this._readWhitespace();
                  }
  
                  return token;
              },
  
              //-----------------------------------------------------------------
              // Animations methods
              //-----------------------------------------------------------------
  
              _keyframes: function(){
  
                  /*
                   * keyframes:
                   *   : KEYFRAMES_SYM S* keyframe_name S* '{' S* keyframe_rule* '}' {
                   *   ;
                   */
                  var tokenStream = this._tokenStream,
                      token,
                      tt,
                      name,
                      prefix = "";
  
                  tokenStream.mustMatch(Tokens.KEYFRAMES_SYM);
                  token = tokenStream.token();
                  if (/^@\-([^\-]+)\-/.test(token.value)) {
                      prefix = RegExp.$1;
                  }
  
                  this._readWhitespace();
                  name = this._keyframe_name();
  
                  this._readWhitespace();
                  tokenStream.mustMatch(Tokens.LBRACE);
  
                  this.fire({
                      type:   "startkeyframes",
                      name:   name,
                      prefix: prefix,
                      line:   token.startLine,
                      col:    token.startCol
                  });
  
                  this._readWhitespace();
                  tt = tokenStream.peek();
  
                  //check for key
                  while(tt == Tokens.IDENT || tt == Tokens.PERCENTAGE) {
                      this._keyframe_rule();
                      this._readWhitespace();
                      tt = tokenStream.peek();
                  }
  
                  this.fire({
                      type:   "endkeyframes",
                      name:   name,
                      prefix: prefix,
                      line:   token.startLine,
                      col:    token.startCol
                  });
  
                  this._readWhitespace();
                  tokenStream.mustMatch(Tokens.RBRACE);
  
              },
  
              _keyframe_name: function(){
  
                  /*
                   * keyframe_name:
                   *   : IDENT
                   *   | STRING
                   *   ;
                   */
                  var tokenStream = this._tokenStream,
                      token;
  
                  tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]);
                  return SyntaxUnit.fromToken(tokenStream.token());
              },
  
              _keyframe_rule: function(){
  
                  /*
                   * keyframe_rule:
                   *   : key_list S*
                   *     '{' S* declaration [ ';' S* declaration ]* '}' S*
                   *   ;
                   */
                  var tokenStream = this._tokenStream,
                      token,
                      keyList = this._key_list();
  
                  this.fire({
                      type:   "startkeyframerule",
                      keys:   keyList,
                      line:   keyList[0].line,
                      col:    keyList[0].col
                  });
  
                  this._readDeclarations(true);
  
                  this.fire({
                      type:   "endkeyframerule",
                      keys:   keyList,
                      line:   keyList[0].line,
                      col:    keyList[0].col
                  });
  
              },
  
              _key_list: function(){
  
                  /*
                   * key_list:
                   *   : key [ S* ',' S* key]*
                   *   ;
                   */
                  var tokenStream = this._tokenStream,
                      token,
                      key,
                      keyList = [];
  
                  //must be least one key
                  keyList.push(this._key());
  
                  this._readWhitespace();
  
                  while(tokenStream.match(Tokens.COMMA)){
                      this._readWhitespace();
                      keyList.push(this._key());
                      this._readWhitespace();
                  }
  
                  return keyList;
              },
  
              _key: function(){
                  /*
                   * There is a restriction that IDENT can be only "from" or "to".
                   *
                   * key
                   *   : PERCENTAGE
                   *   | IDENT
                   *   ;
                   */
  
                  var tokenStream = this._tokenStream,
                      token;
  
                  if (tokenStream.match(Tokens.PERCENTAGE)){
                      return SyntaxUnit.fromToken(tokenStream.token());
                  } else if (tokenStream.match(Tokens.IDENT)){
                      token = tokenStream.token();
  
                      if (/from|to/i.test(token.value)){
                          return SyntaxUnit.fromToken(token);
                      }
  
                      tokenStream.unget();
                  }
  
                  //if it gets here, there wasn't a valid token, so time to explode
                  this._unexpectedToken(tokenStream.LT(1));
              },
  
              //-----------------------------------------------------------------
              // Helper methods
              //-----------------------------------------------------------------
  
              /**
               * Not part of CSS grammar, but useful for skipping over
               * combination of white space and HTML-style comments.
               * @return {void}
               * @method _skipCruft
               * @private
               */
              _skipCruft: function(){
                  while(this._tokenStream.match([Tokens.S, Tokens.CDO, Tokens.CDC])){
                      //noop
                  }
              },
  
              /**
               * Not part of CSS grammar, but this pattern occurs frequently
               * in the official CSS grammar. Split out here to eliminate
               * duplicate code.
               * @param {Boolean} checkStart Indicates if the rule should check
               *      for the left brace at the beginning.
               * @param {Boolean} readMargins Indicates if the rule should check
               *      for margin patterns.
               * @return {void}
               * @method _readDeclarations
               * @private
               */
              _readDeclarations: function(checkStart, readMargins){
                  /*
                   * Reads the pattern
                   * S* '{' S* declaration [ ';' S* declaration ]* '}' S*
                   * or
                   * S* '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S*
                   * Note that this is how it is described in CSS3 Paged Media, but is actually incorrect.
                   * A semicolon is only necessary following a declaration is there's another declaration
                   * or margin afterwards.
                   */
                  var tokenStream = this._tokenStream,
                      tt;
  
  
                  this._readWhitespace();
  
                  if (checkStart){
                      tokenStream.mustMatch(Tokens.LBRACE);
                  }
  
                  this._readWhitespace();
  
                  try {
  
                      while(true){
  
                          if (tokenStream.match(Tokens.SEMICOLON) || (readMargins && this._margin())){
                              //noop
                          } else if (this._declaration()){
                              if (!tokenStream.match(Tokens.SEMICOLON)){
                                  break;
                              }
                          } else {
                              break;
                          }
  
                          //if ((!this._margin() && !this._declaration()) || !tokenStream.match(Tokens.SEMICOLON)){
                          //    break;
                          //}
                          this._readWhitespace();
                      }
  
                      tokenStream.mustMatch(Tokens.RBRACE);
                      this._readWhitespace();
  
                  } catch (ex) {
                      if (ex instanceof SyntaxError && !this.options.strict){
  
                          //fire error event
                          this.fire({
                              type:       "error",
                              error:      ex,
                              message:    ex.message,
                              line:       ex.line,
                              col:        ex.col
                          });
  
                          //see if there's another declaration
                          tt = tokenStream.advance([Tokens.SEMICOLON, Tokens.RBRACE]);
                          if (tt == Tokens.SEMICOLON){
                              //if there's a semicolon, then there might be another declaration
                              this._readDeclarations(false, readMargins);
                          } else if (tt != Tokens.RBRACE){
                              //if there's a right brace, the rule is finished so don't do anything
                              //otherwise, rethrow the error because it wasn't handled properly
                              throw ex;
                          }
  
                      } else {
                          //not a syntax error, rethrow it
                          throw ex;
                      }
                  }
  
              },
  
              /**
               * In some cases, you can end up with two white space tokens in a
               * row. Instead of making a change in every function that looks for
               * white space, this function is used to match as much white space
               * as necessary.
               * @method _readWhitespace
               * @return {String} The white space if found, empty string if not.
               * @private
               */
              _readWhitespace: function(){
  
                  var tokenStream = this._tokenStream,
                      ws = "";
  
                  while(tokenStream.match(Tokens.S)){
                      ws += tokenStream.token().value;
                  }
  
                  return ws;
              },
  
  
              /**
               * Throws an error when an unexpected token is found.
               * @param {Object} token The token that was found.
               * @method _unexpectedToken
               * @return {void}
               * @private
               */
              _unexpectedToken: function(token){
                  throw new SyntaxError("Unexpected token '" + token.value + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol);
              },
  
              /**
               * Helper method used for parsing subparts of a style sheet.
               * @return {void}
               * @method _verifyEnd
               * @private
               */
              _verifyEnd: function(){
                  if (this._tokenStream.LA(1) != Tokens.EOF){
                      this._unexpectedToken(this._tokenStream.LT(1));
                  }
              },
  
              //-----------------------------------------------------------------
              // Validation methods
              //-----------------------------------------------------------------
              _validateProperty: function(property, value){
                  Validation.validate(property, value);
              },
  
              //-----------------------------------------------------------------
              // Parsing methods
              //-----------------------------------------------------------------
  
              parse: function(input){
                  this._tokenStream = new TokenStream(input, Tokens);
                  this._stylesheet();
              },
  
              parseStyleSheet: function(input){
                  //just passthrough
                  return this.parse(input);
              },
  
              parseMediaQuery: function(input){
                  this._tokenStream = new TokenStream(input, Tokens);
                  var result = this._media_query();
  
                  //if there's anything more, then it's an invalid selector
                  this._verifyEnd();
  
                  //otherwise return result
                  return result;
              },
  
              /**
               * Parses a property value (everything after the semicolon).
               * @return {parserlib.css.PropertyValue} The property value.
               * @throws parserlib.util.SyntaxError If an unexpected token is found.
               * @method parserPropertyValue
               */
              parsePropertyValue: function(input){
  
                  this._tokenStream = new TokenStream(input, Tokens);
                  this._readWhitespace();
  
                  var result = this._expr();
  
                  //okay to have a trailing white space
                  this._readWhitespace();
  
                  //if there's anything more, then it's an invalid selector
                  this._verifyEnd();
  
                  //otherwise return result
                  return result;
              },
  
              /**
               * Parses a complete CSS rule, including selectors and
               * properties.
               * @param {String} input The text to parser.
               * @return {Boolean} True if the parse completed successfully, false if not.
               * @method parseRule
               */
              parseRule: function(input){
                  this._tokenStream = new TokenStream(input, Tokens);
  
                  //skip any leading white space
                  this._readWhitespace();
  
                  var result = this._ruleset();
  
                  //skip any trailing white space
                  this._readWhitespace();
  
                  //if there's anything more, then it's an invalid selector
                  this._verifyEnd();
  
                  //otherwise return result
                  return result;
              },
  
              /**
               * Parses a single CSS selector (no comma)
               * @param {String} input The text to parse as a CSS selector.
               * @return {Selector} An object representing the selector.
               * @throws parserlib.util.SyntaxError If an unexpected token is found.
               * @method parseSelector
               */
              parseSelector: function(input){
  
                  this._tokenStream = new TokenStream(input, Tokens);
  
                  //skip any leading white space
                  this._readWhitespace();
  
                  var result = this._selector();
  
                  //skip any trailing white space
                  this._readWhitespace();
  
                  //if there's anything more, then it's an invalid selector
                  this._verifyEnd();
  
                  //otherwise return result
                  return result;
              },
  
              /**
               * Parses an HTML style attribute: a set of CSS declarations
               * separated by semicolons.
               * @param {String} input The text to parse as a style attribute
               * @return {void}
               * @method parseStyleAttribute
               */
              parseStyleAttribute: function(input){
                  input += "}"; // for error recovery in _readDeclarations()
                  this._tokenStream = new TokenStream(input, Tokens);
                  this._readDeclarations();
              }
          };
  
      //copy over onto prototype
      for (prop in additions){
          if (additions.hasOwnProperty(prop)){
              proto[prop] = additions[prop];
          }
      }
  
      return proto;
  }();
  
  
  /*
  nth
    : S* [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]? |
           ['-'|'+']? INTEGER | {O}{D}{D} | {E}{V}{E}{N} ] S*
    ;
  */
  