private CssToken ScanSubstringMatch() { CssToken token = null; if (PeekChar() == '=') { // skip the two characters and return a substring match NextChar(); NextChar(); token = new CssToken(TokenType.SubstringMatch, c_substringMatch, m_context); } else { // see if this asterisk is a namespace portion of an identifier token = ScanIdent(); } if (token == null) { // skip the * and return a character token NextChar(); token = new CssToken(TokenType.Character, '*', m_context); } return token; }
private CssToken ScanSuffixMatch() { CssToken token = null; NextChar(); if (m_currentChar == '=') { NextChar(); token = new CssToken(TokenType.SuffixMatch, c_suffixMatch, m_context); } return (token != null ? token : new CssToken(TokenType.Character, '$', m_context)); }
private CssToken ScanNum() { CssToken token = null; string num = GetNum(); if (num != null) { if (m_currentChar == '%') { NextChar(); // let's always keep the percentage on the number, even if it's // zero -- some things require it (like the rgb function) token = new CssToken( TokenType.Percentage, num + '%', m_context ); // and make sure the "raw number" we keep has it as well m_rawNumber += '%'; } else { string dimen = GetIdent(); if (dimen == null) { // if there is no identifier, it's a num. token = new CssToken(TokenType.Number, num, m_context); } else { // add the dimension to the raw number m_rawNumber += dimen; // classify the dimension type TokenType tokenType = TokenType.Dimension; switch (dimen.ToUpperInvariant()) { case "EM": // font-size of the element case "EX": // x-height of the element's font case "CH": // width of the zero glyph in the element's font case "REM": // font-size of the root element case "VW": // viewport's width case "VH": // viewport's height case "VM": // viewport width or height, whichever is smaller of the two (use VMIN) case "VMIN": // minimum of the viewport's height and width case "VMAX": // maximum of the viewport's height and width case "FR": // fraction of available space case "GR": // grid unit case "GD": // text grid unit tokenType = TokenType.RelativeLength; break; case "CM": // centimeters case "MM": // millimeters case "IN": // inches (1in == 2.54cm) case "PX": // pixels (1px == 1/96in) case "PT": // points (1pt == 1/72in) case "PC": // picas (1pc == 12pt) tokenType = TokenType.AbsoluteLength; break; case "DEG": // degrees (360deg == 1 full circle) case "GRAD": // gradians (400grad == 1 full circle) case "RAD": // radians (2*pi radians == 1 full circle) case "TURN": // turns (1turn == 1 full circle) tokenType = TokenType.Angle; break; case "MS": // milliseconds case "S": // seconds tokenType = TokenType.Time; break; case "DPI": // dots per inch case "DPCM": // dots per centimeter case "DPPX": // dots per pixel tokenType = TokenType.Resolution; break; case "HZ": // hertz case "KHZ": // kilohertz tokenType = TokenType.Frequency; break; case "DB": // decibel case "ST": // semitones tokenType = TokenType.Speech; break; } // if the number is zero, it really doesn't matter what the dimensions are so we can remove it // EXCEPT for Angles, Times, Frequencies, and Resolutions - they should not get their units // stripped, since we can only do that for LENGTHS as per the specs. // And percentages, since 0% is the same as 0. (true?) // ALSO, if we don't recognize the dimension, leave it -- it could be a browser hack or some // other intentional construct. if (num == "0" && tokenType != TokenType.Dimension && tokenType != TokenType.Angle && tokenType != TokenType.Time && tokenType != TokenType.Frequency && tokenType != TokenType.Resolution) { token = new CssToken(TokenType.Number, num, m_context); } else { token = new CssToken(tokenType, num + dimen, m_context); } } } } else if (m_currentChar == '.') { token = new CssToken(TokenType.Character, '.', m_context); NextChar(); } else { // this function is only called when the first character is // a digit or a period. So this block should never execute, since // a digit will produce a num, and if it doesn't, the previous block // picks up the period. ReportError(1, CssErrorCode.UnexpectedNumberCharacter, m_currentChar); } return token; }
private CssToken ScanProgId() { CssToken token = null; StringBuilder sb = new StringBuilder(); sb.Append("progid:"); string ident = GetIdent(); while (ident != null) { sb.Append(ident); if (m_currentChar == '.') { sb.Append('.'); NextChar(); } ident = GetIdent(); } if (m_currentChar == '(') { sb.Append('('); NextChar(); token = new CssToken(TokenType.ProgId, sb.ToString(), m_context); } else { ReportError(1, CssErrorCode.ExpectedOpenParenthesis); } return token; }
private CssToken ScanImportant() { CssToken token = null; NextChar(); string w = GetW(); if (char.ToUpperInvariant(m_currentChar) == 'I') { if (ReadString("IMPORTANT")) { // no matter what the case or whether or not there is space between the ! and // the important, we're going to represent this token as having no space and all // lower-case. token = new CssToken(TokenType.ImportantSymbol, "!important", m_context); } } // if the token is still null but we had found some whitespace, // we need to push a whitespace char back onto the read-ahead if (token == null && w.Length > 0) { PushChar(' '); } return (token != null ? token : new CssToken(TokenType.Character, '!', m_context)); }
private CssToken ScanIncludes() { CssToken token = null; NextChar(); if (m_currentChar == '=') { NextChar(); token = new CssToken(TokenType.Includes, c_scanIncludes, m_context); } return (token != null ? token : new CssToken(TokenType.Character, '~', m_context)); }
private CssToken ScanDashMatch() { CssToken token = null; // if the next character is an equals sign, then we have a dash-match if (PeekChar() == '=') { // skip the two characters NextChar(); NextChar(); token = new CssToken(TokenType.DashMatch, c_dashMatch, m_context); } else { // see if this is the start of a namespace ident token = ScanIdent(); } // if we haven't computed a token yet, it's just a character if (token == null) { NextChar(); token = new CssToken(TokenType.Character, '|', m_context); } return token; }
private string NextSignificantToken() { // MOST of the time we won't need to save anything, // so don't bother allocating a string builder unless we need it StringBuilder sb = null; // get the next token m_currentToken = m_scanner.NextToken(); m_encounteredNewLine = m_scanner.GotEndOfLine; while (CurrentTokenType == TokenType.Space || CurrentTokenType == TokenType.Comment) { // if this token is a comment, add it to the builder if (CurrentTokenType == TokenType.Comment) { // check for important comment string commentText = CurrentTokenText; bool importantComment = commentText.StartsWith("/*!", StringComparison.Ordinal); if (importantComment) { // get rid of the exclamation mark in some situations commentText = NormalizeImportantComment(commentText); } // if the comment mode is none, don't ever output it. // if the comment mode is all, always output it. // otherwise only output it if it is an important comment. bool writeComment = Settings.CommentMode == CssComment.All || (importantComment && Settings.CommentMode != CssComment.None); if (!importantComment) { // see if this is a value-replacement id Match match = s_valueReplacement.Match(commentText); if (match.Success) { // check all the resource strings objects to see if one is a match. m_valueReplacement = null; var resourceList = Settings.ResourceStrings; if (resourceList.Count > 0) { // get the id of the string we want to substitute string ident = match.Result("${id}"); // walk the list BACKWARDS so later resource string objects override previous ones for (var ndx = resourceList.Count - 1; ndx >= 0; --ndx) { m_valueReplacement = resourceList[ndx][ident]; if (m_valueReplacement != null) { break; } } } // if there is such a string, we will have saved the value in the value replacement // variable so it will be substituted for the next value. // if there is no such string, we ALWAYS want to output the comment so we know // there was a problem (even if the comments mode is to output none) writeComment = m_valueReplacement == null; if (writeComment) { // make sure the comment is normalized commentText = NormalizedValueReplacementComment(commentText); } } } if (writeComment) { // if we haven't yet allocated a string builder, do it now if (sb == null) { sb = new StringBuilder(); } // add the comment to the builder sb.Append(commentText); } } // next token m_currentToken = m_scanner.NextToken(); m_encounteredNewLine = m_encounteredNewLine || m_scanner.GotEndOfLine; } // return any comments we found in the mean time return (sb == null ? string.Empty : sb.ToString()); }
private CssToken ScanCDO() { CssToken token = null; NextChar(); // points to !? if (m_currentChar == '!') { if (PeekChar() == '-') { NextChar(); // points to - if (PeekChar() == '-') { NextChar(); // points to second hyphen NextChar(); token = new CssToken(TokenType.CommentOpen, c_commentStart, m_context); } else { // we want to just return the < character, but // we're currently pointing to the first hyphen, // so we need to add the ! to the read ahead buffer PushChar('!'); } } } return (token != null ? token : token = new CssToken(TokenType.Character, '<', m_context)); }
private CssToken ScanComment() { CssToken token = null; NextChar(); if (m_currentChar == '*') { NextChar(); // everything is a comment until we get to */ StringBuilder sb = new StringBuilder(); sb.Append("/*"); bool terminated = false; while (m_currentChar != '\0') { sb.Append(m_currentChar); if (m_currentChar == '*' && PeekChar() == '/') { sb.Append('/'); NextChar(); // now points to / NextChar(); // now points to following character // check for comment-hack 2 -- NS4 sees /*/*//*/ as a single comment // while everyone else properly parses that as two comments, which hides everything // after this construct until the next comment. So this hack shows the stuff // between ONLY to NS4. But we still want to crunch it, so if we just found // a comment like /*/*/, check to see if the next characters are /*/. If so, // treat it like the single comment NS4 sees. // (and don't forget that if we want to keep them, we've turned them both into important comments) if (sb.ToString() == "/*!/*/" && ReadString("/*/")) { // read string will leave the current character after the /*/ string, // so add the part we just read to the string builder and we'll break // out of the loop sb.Append("/*/"); } terminated = true; break; } NextChar(); } if (!terminated) { ReportError(0, CssErrorCode.UnterminatedComment); } var comment = sb.ToString(); if (string.Compare(comment, 2, "/#SOURCE", 0, 8, StringComparison.OrdinalIgnoreCase) == 0) { // found our special comment: /*/#SOURCE line col path */ var match = s_sourceDirective.Match(comment); if (match != null) { int line, column; if (int.TryParse(match.Result("${line}"), out line) && int.TryParse(match.Result("${col}"), out column)) { // we got a proper line, column, and non-blank path. reset our context // with the new line and column. this.OnContextChange( match.Result("${path}"), line, column); // now, this is weird. by AjaxMin convention, there should be NOTHING after this comment // but whitespace and a single line-terminator. So we will skip EVERYTHING after this comment up // to the first line-terminator, and then eat that first line-terminator. So it's possible to // completely ignore code by putting it between this multiline comment and the end of its line. SkipToNextLineWithoutUpdate(); // return null so this token gets skipped return null; } } } token = new CssToken(TokenType.Comment, comment, m_context); } else if (m_currentChar == '/') { // we found '//' -- it's a JS-style single-line comment which isn't strictly // supported by CSS, but we're going to treat as a valid comment because // developers like using them. We're not going to persist them, though -- we're // going to eat these comments, since they're not valid CSS. // first check for our special ///#source directive. // We're on the second slash; see if the NEXT character is a third slash if (PeekChar() == '/') { // found '///' NextChar(); // now w're on the third slash; see if the NEXT character is a pound-sign if (PeekChar() == '#') { // okay, we have ///#, which we are going to reserve for all AjaxMin directive comments. // so the source better not have something meaningful for the rest of the line. NextChar(); // now we're on the pound-sign. See if we have the source directive if (ReadString("#SOURCE")) { // we have a source directive: ///#source line col file // skip space DirectiveSkipSpace(); // pull the line and column numbers. Must be positive integers greater than zero. int line = DirectiveScanInteger(); if (line > 0) { DirectiveSkipSpace(); int column = DirectiveScanInteger(); if (column > 0) { DirectiveSkipSpace(); // the rest of the comment line is the file path. var sb = new StringBuilder(); while (m_currentChar != '\n' && m_currentChar != '\r') { sb.Append(m_currentChar); DirectiveNextChar(); } var fileContext = sb.ToString().TrimEnd(); if (!string.IsNullOrEmpty(fileContext)) { // we got a proper line, column, and non-blank path. reset our context // with the new line and column. this.OnContextChange(fileContext, line, column); // START SPECIAL PROCESSING SkipToNextLineWithoutUpdate(); // return null here so we don't fall through and return a / character. return null; } } } } } } // eat the comment up to, but not including, the next line terminator while (m_currentChar != '\n' && m_currentChar != '\r' && m_currentChar != '\0') { NextChar(); } // if we wanted to maintain these comments, we would set the token // variable to a new CssToken object of type comment. But we don't, so // just return null so that the scanner will go around again. return null; } if (token == null) { // not a comment token = new CssToken(TokenType.Character, '/', m_context); } return token; }
private CssToken ScanCDC() { CssToken token = null; NextChar(); // points to second hyphen? if (m_currentChar == '-') { if (PeekChar() == '>') { NextChar(); // points to > NextChar(); token = new CssToken(TokenType.CommentClose, c_commentEnd, m_context); } } return token; }
public CssToken NextToken() { GotEndOfLine = false; // advance the context m_context.Advance(); m_rawNumber = null; CssToken token = null; bool tryAgain; do { tryAgain = false; switch (m_currentChar) { case '\0': // end of file m_isAtEOF = true; break; case '\r': case '\n': case '\f': // we hit an end-of-line character, but treat it like any other whitespace GotEndOfLine = true; goto case ' '; case ' ': case '\t': // no matter how much whitespace is actually in // the stream, we're just going to encode a single // space in the token itself while (IsSpace(m_currentChar)) { if (m_currentChar == '\r' || m_currentChar == '\n' || m_currentChar == '\f') { GotEndOfLine = true; } NextChar(); } token = new CssToken(TokenType.Space, ' ', m_context); break; case '/': token = ScanComment(); if (token == null) { // this could happen if we processed an ajaxmin directive. // go around again and try for the next token tryAgain = true; } break; case '<': if (AllowEmbeddedAspNetBlocks && PeekChar() == '%') { token = ScanAspNetBlock(); } else { token = ScanCDO(); } break; case '-': token = ScanCDC(); if (token == null) { // identifier in CSS2.1 and CSS3 can start with a hyphen // to indicate vendor-specific identifiers. string ident = GetIdent(); if (ident != null) { // vendor-specific identifier // but first see if it's a vendor-specific function! if (m_currentChar == '(') { // it is -- consume the parenthesis; it's part of the token NextChar(); token = new CssToken(TokenType.Function, "-" + ident + '(', m_context); } else { // nope -- just a regular identifier token = new CssToken(TokenType.Identifier, "-" + ident, m_context); } } else { // just a hyphen character token = new CssToken(TokenType.Character, '-', m_context); } } break; case '~': token = ScanIncludes(); break; case '|': token = ScanDashMatch(); break; case '^': token = ScanPrefixMatch(); break; case '$': token = ScanSuffixMatch(); break; case '*': token = ScanSubstringMatch(); break; case '\'': case '"': token = ScanString(); break; case '#': token = ScanHash(); break; case '@': token = ScanAtKeyword(); break; case '!': token = ScanImportant(); break; case 'U': case 'u': token = ScanUrl(); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': token = ScanNum(); break; default: token = ScanIdent(); break; } } while (tryAgain); return token; }
// skip to the next token, but output any comments we may find as we go along private TokenType NextToken() { m_currentToken = m_scanner.NextToken(); m_encounteredNewLine = m_scanner.GotEndOfLine; while (CurrentTokenType == TokenType.Comment) { // the append statement might not actually append anything. // if it doesn't, we don't need to output a newline if (AppendCurrent()) { NewLine(); } m_currentToken = m_scanner.NextToken(); m_encounteredNewLine = m_encounteredNewLine || m_scanner.GotEndOfLine; } return CurrentTokenType; }
private CssToken ScanUnicodeRange() { // when called, the current character is the character *after* U+ CssToken token = null; StringBuilder sb = new StringBuilder(); sb.Append("U+"); bool hasQuestions = false; int count = 0; bool leadingZero = true; int firstValue = 0; while (m_currentChar != '\0' && count < 6 && (m_currentChar == '?' || (!hasQuestions && IsH(m_currentChar)))) { // if this isn't a leading zero, reset the flag if (leadingZero && m_currentChar != '0') { leadingZero = false; } if (m_currentChar == '?') { hasQuestions = true; // assume the digit is an "F" for maximum value firstValue = firstValue*16 + HValue('F'); } else { firstValue = firstValue*16 + HValue(m_currentChar); } if (!leadingZero) { sb.Append(m_currentChar); } ++count; NextChar(); } if (count > 0) { // if the unicode value is out of range, throw an error if (firstValue < 0 || 0x10ffff < firstValue) { // throw an error ReportError(0, CssErrorCode.InvalidUnicodeRange, sb.ToString()); } // if we still have the leading zero flag, then all the numbers were zero // and we didn't output any of them. if (leadingZero) { // add one zero to keep it proper sb.Append('0'); } if (hasQuestions) { // if there are question marks, then we're done token = new CssToken( TokenType.UnicodeRange, sb.ToString(), m_context); } else if (m_currentChar == '-') { sb.Append('-'); NextChar(); count = 0; leadingZero = true; int secondValue = 0; while (m_currentChar != '\0' && count < 6 && IsH(m_currentChar)) { // if this isn't a leading zero, reset the flag if (leadingZero && m_currentChar != '0') { leadingZero = false; } secondValue = secondValue * 16 + HValue(m_currentChar); if (!leadingZero) { sb.Append(m_currentChar); } ++count; NextChar(); } if (count > 0) { // if we still have the leading zero flag, then all the numbers were zero // and we didn't output any of them. if (leadingZero) { // add one zero to keep it proper sb.Append('0'); } // check to make sure the second value is within range // AND is greater than the first if (secondValue < 0 || 0x10ffff < secondValue || firstValue >= secondValue) { // throw an error ReportError(0, CssErrorCode.InvalidUnicodeRange, sb.ToString()); } token = new CssToken( TokenType.UnicodeRange, sb.ToString(), m_context); } } else { // single code-point with at least one character token = new CssToken( TokenType.UnicodeRange, sb.ToString(), m_context); } } // if we don't hve a unicode range, // we need to return an ident token from the U we already scanned if (token == null) { // push everything back onto the buffer PushString(sb.ToString()); token = ScanIdent(); } return token; }
private CssToken ScanIdent() { CssToken token = null; string ident = GetIdent(); if (ident != null) { if (m_currentChar == '(') { NextChar(); if (string.Compare(ident, "not", StringComparison.OrdinalIgnoreCase) == 0) { token = new CssToken(TokenType.Not, ident + '(', m_context); } else { token = new CssToken(TokenType.Function, ident + '(', m_context); } } else if (string.Compare(ident, "progid", StringComparison.OrdinalIgnoreCase) == 0 && m_currentChar == ':') { NextChar(); token = ScanProgId(); } else { token = new CssToken(TokenType.Identifier, ident, m_context); } } // if we failed somewhere in the processing... if (ident == null) { if (m_currentChar != '\0') { // create a character token token = new CssToken(TokenType.Character, m_currentChar, m_context); NextChar(); } } return token; }
private CssToken ScanUrl() { CssToken token = null; if (PeekChar() == '+') { NextChar(); // now current is the + NextChar(); // now current is the first character after the + token = ScanUnicodeRange(); } else if (ReadString("URL(")) { StringBuilder sb = new StringBuilder(); sb.Append("url("); GetW(); string url = GetString(); if (url == null) { url = GetUrl(); } if (url != null) { sb.Append(url); GetW(); if (m_currentChar == ')') { sb.Append(')'); NextChar(); token = new CssToken( TokenType.Uri, sb.ToString(), m_context ); } } } return (token != null ? token : ScanIdent()); }
// just skip to the next token; don't skip over comments private TokenType NextRawToken() { m_currentToken = m_scanner.NextToken(); m_encounteredNewLine = m_scanner.GotEndOfLine; return CurrentTokenType; }