in AjaxMinDll/Css/CssParser.cs [4045:4555]
private bool Append(object obj, TokenType tokenType)
{
bool outputText = false;
bool textEndsInEscapeSequence = false;
// if the no-output flag is true, don't output anything
// or process value replacement comments
if (!m_noOutput)
{
var parsed = m_builders.Peek();
var text = obj.ToString();
// first if there are replacement tokens in the settings, then we'll want to
// replace any tokens with the appropriate replacement values
if (Settings.ReplacementTokens.Count > 0)
{
text = CommonData.ReplacementToken.Replace(text, GetReplacementValue);
}
if (tokenType == TokenType.Identifier || tokenType == TokenType.Dimension)
{
// need to make sure invalid identifier characters are properly escaped
StringBuilder escapedBuilder = null;
try
{
var startIndex = 0;
var protectNextHexCharacter = false;
var firstIndex = 0;
// if the token type is an identifier, we need to make sure the first character
// is a proper identifier start, or is escaped. But if it's a dimension, the first
// character will be a numeric digit -- which wouldn't be a valid identifier. So
// for dimensions, skip the first character -- subsequent numeric characters will
// be okay.
if (tokenType == TokenType.Identifier)
{
// for identifiers, if the first character is a hyphen or an underscore, then it's a prefix
// and we want to look at the next character for nmstart.
firstIndex = text[0] == '_' || text[0] == '-' ? 1 : 0;
if (firstIndex < text.Length)
{
// the only valid non-escaped first characters are A-Z (and a-z)
var firstChar = text[firstIndex];
// anything at or above 0x80 is okay for identifiers
if (firstChar < 0x80)
{
// if it's not an a-z or A-Z, we want to escape it
// also leave literal back-slashes as-is, too. The identifier might start with an escape
// sequence that we didn't decode to its Unicode character for whatever reason.
if ((firstChar < 'A' || 'Z' < firstChar)
&& (firstChar < 'a' || 'z' < firstChar)
&& firstChar != '\\')
{
// invalid first character -- create the string builder
escapedBuilder = StringBuilderPool.Acquire();
// if we had a prefix, output it
if (firstIndex > 0)
{
escapedBuilder.Append(text[0]);
}
// output the escaped first character
protectNextHexCharacter = EscapeCharacter(escapedBuilder, text[firstIndex]);
textEndsInEscapeSequence = true;
startIndex = firstIndex + 1;
}
}
}
}
else
{
// for dimensions, we want to skip over the numeric part. So any sign, then decimal
// digits, then a decimal point (period), then decimal digits. The rest will be the identifier
// part that we want to escape.
if (text[0] == '+' || text[0] == '-')
{
++firstIndex;
}
while ('0' <= text[firstIndex] && text[firstIndex] <= '9')
{
++firstIndex;
}
if (text[firstIndex] == '.')
{
++firstIndex;
}
while ('0' <= text[firstIndex] && text[firstIndex] <= '9')
{
++firstIndex;
}
// since we start at the first character AFTER firstIndex, subtract
// one so we get back to the first character that isn't a part of
// the number portion
--firstIndex;
}
// loop through remaining characters, escaping any invalid nmchar characters
for (var ndx = firstIndex + 1; ndx < text.Length; ++ndx)
{
char nextChar = text[ndx];
// anything at or above 0x80, then it's okay and doesnt need to be escaped
if (nextChar < 0x80)
{
// only -, _, 0-9, a-z, A-Z are allowed without escapes
// but we also want to NOT escape \ or space characters. If the identifier had
// an escaped space character, it will still be escaped -- so any spaces would
// be necessary whitespace for the end of unicode escapes.
if (nextChar == '\\')
{
// escape characters cause the next character -- no matter what it is -- to
// be part of the escape and not escaped itself. Even if this is part of a
// unicode or character escape, this will hold true. Increment the index and
// loop around again so that we skip over both the backslash and the following
// character.
++ndx;
}
else if (nextChar != '-'
&& nextChar != '_'
&& nextChar != ' '
&& ('0' > nextChar || nextChar > '9')
&& ('a' > nextChar || nextChar > 'z')
&& ('A' > nextChar || nextChar > 'Z'))
{
// need to escape this character -- create the builder if we haven't already
if (escapedBuilder == null)
{
escapedBuilder = StringBuilderPool.Acquire();
}
// output any okay characters we have so far
if (startIndex < ndx)
{
// if the first character of the unescaped string is a valid hex digit,
// then we need to add a space so that characer doesn't get parsed as a
// digit in the previous escaped sequence.
// and if the first character is a space, we need to protect it from the
// previous escaped sequence with another space, too.
string unescapedSubstring = text.Substring(startIndex, ndx - startIndex);
if ((protectNextHexCharacter && CssScanner.IsH(unescapedSubstring[0]))
|| (textEndsInEscapeSequence && unescapedSubstring[0] == ' '))
{
escapedBuilder.Append(' ');
}
escapedBuilder.Append(unescapedSubstring);
}
// output the escape sequence for the current character
protectNextHexCharacter = EscapeCharacter(escapedBuilder, text[ndx]);
textEndsInEscapeSequence = true;
// update the start pointer to the next character
startIndex = ndx + 1;
}
}
}
// if we escaped anything, get the text from what we built
if (escapedBuilder != null)
{
// append whatever is left over
if (startIndex < text.Length)
{
// if the first character of the unescaped string is a valid hex digit,
// then we need to add a space so that characer doesn't get parsed as a
// digit in the previous escaped sequence.
// same for spaces! a trailing space will be part of the escape, so if we need
// a real space to follow, need to make sure there are TWO.
string unescapedSubstring = text.Substring(startIndex);
if ((protectNextHexCharacter && CssScanner.IsH(unescapedSubstring[0]))
|| unescapedSubstring[0] == ' ')
{
escapedBuilder.Append(' ');
}
escapedBuilder.Append(unescapedSubstring);
textEndsInEscapeSequence = false;
}
// get the full string
text = escapedBuilder.ToString();
}
}
finally
{
escapedBuilder.Release();
}
}
else if (tokenType == TokenType.String)
{
// we need to make sure that control codes are properly escaped
StringBuilder sb = null;
try
{
var startRaw = 0;
for (var ndx = 0; ndx < text.Length; ++ndx)
{
// if it's a control code...
var ch = text[ndx];
if (ch < ' ')
{
// if we haven't created our string builder yet, do it now
if (sb == null)
{
sb = StringBuilderPool.Acquire();
}
// add the raw text up to but not including the current character.
// but only if start raw is BEFORE the current index
if (startRaw < ndx)
{
sb.Append(text.Substring(startRaw, ndx - startRaw));
}
// regular unicode escape
sb.Append("\\{0:x}".FormatInvariant(char.ConvertToUtf32(text, ndx)));
// if the NEXT character (if there is one) is a hex digit,
// we will need to append a space to signify the end of the escape sequence, since this
// will never have more than two digits (0 - 1f).
if (ndx + 1 < text.Length
&& CssScanner.IsH(text[ndx + 1]))
{
sb.Append(' ');
}
// and update the raw pointer to the next character
startRaw = ndx + 1;
}
}
// if we have something left over, add the rest now
if (sb != null && startRaw < text.Length)
{
sb.Append(text.Substring(startRaw));
}
// if we built up a string, use it. Otherwise just use what we have.
text = sb == null ? text : sb.ToString();
}
finally
{
sb.Release();
}
}
else if (tokenType == TokenType.Uri && Settings.FixIE8Fonts)
{
// IE8 @font-face directive has an issue with src properties that are URLs ending with .EOT
// that don't have any querystring. They end up sending a malformed HTTP request to the server,
// which is bad for the server. So we want to automatically fix this for developers: if ANY URL
// ends in .EOT without a querystring parameters, just add a question mark in the appropriate
// location. This fixes the IE8 issue.
text = s_eotIE8Fix.Replace(text, ".eot?$1");
}
// if it's not a comment, we're going to output it.
// if it is a comment, we're not going to SAY we've output anything,
// even if we end up outputting the comment
var isImportant = false;
outputText = (tokenType != TokenType.Comment);
if (!outputText)
{
// if the comment mode is none, we never want to output it.
// if the comment mode is all, then we always want to output it.
// otherwise we only want to output if it's an important /*! */ comment
if (text.StartsWith("/*!", StringComparison.Ordinal))
{
// this is an important comment. We will always output it
// UNLESS the comment mode is none. If it IS none, bail now.
if (Settings.CommentMode == CssComment.None)
{
return false;
}
// this is an important comment that we always want to output
// (after we get rid of the exclamation point in some situations)
text = NormalizeImportantComment(text);
// find the index of the initial / character
var indexSlash = text.IndexOf('/');
if (indexSlash > 0)
{
// it's not the first character!
// the only time that should happen is if we put a line-feed in front.
// if the string builder is empty, or if the LAST character is a \r or \n,
// then trim off everything before that opening slash
if (m_outputNewLine)
{
// trim off everything before it
text = text.Substring(indexSlash);
}
}
}
else if (s_sharepointReplacement.IsMatch(text))
{
// if it's a sharepoint replacement comment, then always output it
// (unless settings say NO comments)
if (Settings.CommentMode == CssComment.None)
{
return false;
}
}
else
{
// not important, and not sharepoint.
// check to see if it's a special value-replacement comment
Match match = s_valueReplacement.Match(CurrentTokenText);
if (match.Success)
{
m_valueReplacement = null;
var resourceList = Settings.ResourceStrings;
if (resourceList.Count > 0)
{
// it is! see if we have a replacement string
string id = match.Result("${id}");
// if we have resource strings in the settings, check each one for the
// id and set the value replacement field to the value.
// walk backwards so later objects override earlier ones.
for (var ndx = resourceList.Count - 1; ndx >= 0; --ndx)
{
m_valueReplacement = resourceList[ndx][id];
if (m_valueReplacement != null)
{
break;
}
}
}
if (m_valueReplacement != null)
{
// we do. Don't output the comment. Instead, save the value replacement
// for the next time we encounter a value
return false;
}
else
{
// make sure the comment is normalized
text = NormalizedValueReplacementComment(text);
}
}
else if (Settings.CommentMode != CssComment.All)
{
// don't want to output, bail now
return false;
}
}
// see if it's still important
isImportant = text.StartsWith("/*!", StringComparison.Ordinal);
}
else if (m_parsingColorValue
&& (tokenType == TokenType.Identifier || tokenType == TokenType.ReplacementToken))
{
if (!text.StartsWith("#", StringComparison.Ordinal))
{
bool nameConvertedToHex = false;
string lowerCaseText = text.ToLowerInvariant();
string rgbString;
switch (Settings.ColorNames)
{
case CssColor.Hex:
// we don't want any color names in our code.
// convert ALL known color names to hex, so see if there is a match on
// the set containing all the name-to-hex values
if (ColorSlice.AllColorNames.TryGetValue(lowerCaseText, out rgbString))
{
text = rgbString;
nameConvertedToHex = true;
}
break;
case CssColor.Strict:
// we only want strict names in our css.
// convert all non-strict name to hex, AND any strict names to hex if the hex is
// shorter than the name. So check the set that contains all non-strict name-to-hex
// values and all the strict name-to-hex values where hex is shorter than name.
if (ColorSlice.StrictHexShorterThanNameAndAllNonStrict.TryGetValue(lowerCaseText, out rgbString))
{
text = rgbString;
nameConvertedToHex = true;
}
break;
case CssColor.Major:
// we don't care if there are non-strict color name. So check the set that only
// contains name-to-hex pairs where the hex is shorter than the name.
if (ColorSlice.HexShorterThanName.TryGetValue(lowerCaseText, out rgbString))
{
text = rgbString;
nameConvertedToHex = true;
}
break;
case CssColor.NoSwap:
// nope; leave it a name and don't swap it with the equivalent hex value
break;
}
// if we didn't convert the color name to hex, let's see if it is a color
// name -- if so, we want to make it lower-case for readability. We don't need
// to do this check if our color name setting is hex-only, because we would
// have already converted the name if we know about it
if (Settings.ColorNames != CssColor.Hex && !nameConvertedToHex
&& ColorSlice.AllColorNames.TryGetValue(lowerCaseText, out rgbString))
{
// the color exists in the table, so we're pretty sure this is a color.
// make sure it's lower case
text = lowerCaseText;
}
}
else if (CurrentTokenType == TokenType.ReplacementToken)
{
// a replacement token is a color hash -- make sure we trim it to #RGB if it matches #RRGGBB
text = CrunchHexColor(text, Settings.ColorNames, m_noColorAbbreviation);
}
}
// if the global might-need-space flag is set and the first character we're going to
// output if a hex digit or a space, we will need to add a space before our text
if (m_mightNeedSpace
&& (CssScanner.IsH(text[0]) || text[0] == ' '))
{
if (m_lineLength >= Settings.LineBreakThreshold)
{
// we want to add whitespace, but we're over the line-length threshold, so
// output a line break instead
AddNewLine();
}
else
{
// output a space on the same line
parsed.Append(' ');
++m_lineLength;
}
}
if (tokenType == TokenType.Comment && isImportant)
{
// don't bother resetting line length after this because
// we're going to follow the comment with another blank line
// and we'll reset the length at that time
AddNewLine();
}
if (text == " ")
{
// we are asking to output a space character. At this point, if we are
// over the line-length threshold, we can substitute a line break for a space.
if (m_lineLength >= Settings.LineBreakThreshold)
{
AddNewLine();
}
else
{
// just output a space, and don't change the newline flag
parsed.Append(' ');
++m_lineLength;
}
}
else
{
// normal text
// see if we wanted to force a newline
if (m_forceNewLine)
{
// only output a newline if we aren't already on a new line
// AND we are in multiple-line mode
if (!m_outputNewLine && Settings.OutputMode == OutputMode.MultipleLines)
{
AddNewLine();
}
// reset the flag
m_forceNewLine = false;
}
parsed.Append(text);
m_outputNewLine = false;
if (tokenType == TokenType.Comment && isImportant)
{
AddNewLine();
m_lineLength = 0;
m_outputNewLine = true;
}
else
{
m_lineLength += text.Length;
}
}
// if the text we just output ENDS in an escape, we might need a space later
m_mightNeedSpace = textEndsInEscapeSequence;
// save a copy of the string so we can check the last output
// string later if we need to
m_lastOutputString = text;
}
return outputText;
}