private bool Append()

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;
        }