/// <summary>
        /// Upgrades a META content-type tag to the META charset tag
        /// </summary>
        /// <param name="tag">META content-type tag</param>
        /// <returns>META charset tag</returns>
        private HtmlTag UpgradeToMetaCharsetTag(HtmlTag tag)
        {
            HtmlTag upgradedTag = tag;

            HtmlAttribute contentAttribute = tag.Attributes.SingleOrDefault(a => a.Name == "content");
            if (contentAttribute != null)
            {
                string content = contentAttribute.Value.Trim();
                if (content.Length > 0)
                {
                    Match contentMatch = _metaContentTypeTagValueRegex.Match(content);
                    if (contentMatch.Success)
                    {
                        string charset = contentMatch.Groups["charset"].Value;
                        upgradedTag = new HtmlTag(tag.Name,
                            new List<HtmlAttribute> {new HtmlAttribute("charset", charset, HtmlAttributeType.Text)},
                            tag.Flags);
                    }
                }
            }

            return upgradedTag;
        }
        /// <summary>
        /// Builds a string representation of the attribute
        /// </summary>
        /// <param name="context">Markup parsing context</param>
        /// <param name="attribute">HTML attribute</param>
        /// <param name="tag">HTML tag</param>
        /// <returns>String representation of the attribute</returns>
        private string BuildAttributeString(MarkupParsingContext context, HtmlTag tag, HtmlAttribute attribute)
        {
            string tagName = tag.Name;
            HtmlTagFlags tagFlags = tag.Flags;
            IList<HtmlAttribute> attributes = tag.Attributes;

            string attributeString;
            string attributeName = attribute.Name;
            string attributeValue = attribute.Value;
            bool attributeHasValue = attribute.HasValue;
            HtmlAttributeType attributeType = attribute.Type;
            bool useHtmlSyntax = !_settings.UseXhtmlSyntax;

            if (useHtmlSyntax && IsXmlAttribute(attributeName))
            {
                string sourceCode = context.SourceCode;
                SourceCodeNodeCoordinates attributeCoordinates = attribute.NameCoordinates;

                WriteWarning(LogCategoryConstants.HtmlMinificationWarning,
                    string.Format(Strings.WarningMessage_XmlBasedAttributeNotAllowed, attributeName), _fileContext,
                    attributeCoordinates.LineNumber, attributeCoordinates.ColumnNumber,
                    SourceCodeNavigator.GetSourceFragment(sourceCode, attributeCoordinates));
            }

            if ((_settings.RemoveRedundantAttributes && IsAttributeRedundant(tagName, attributeName, attributeValue, attributes))
                || (_settings.RemoveJsTypeAttributes && IsJavaScriptTypeAttribute(tagName, attributeName, attributeValue))
                || (_settings.RemoveCssTypeAttributes && IsCssTypeAttribute(tagName, attributeName, attributeValue, attributes))
                || (useHtmlSyntax && CanRemoveXmlAttribute(tagName, attributeName)))
            {
                attributeString = string.Empty;
                return attributeString;
            }

            bool isCustomBooleanAttribute = (!attributeHasValue && attributeType == HtmlAttributeType.Text);
            if (isCustomBooleanAttribute && useHtmlSyntax)
            {
                attributeString = " " + attributeName;
                return attributeString;
            }

            if (attributeType == HtmlAttributeType.Boolean)
            {
                if (_settings.CollapseBooleanAttributes)
                {
                    attributeString = " " + attributeName;
                    return attributeString;
                }

                attributeValue = attributeName;
            }
            else if (isCustomBooleanAttribute)
            {
                attributeValue = string.Empty;
            }
            else
            {
                attributeValue = CleanAttributeValue(context, tag, attribute);

                if (_settings.RemoveEmptyAttributes
                    && CanRemoveEmptyAttribute(tagName, attributeName, attributeValue, attributeType))
                {
                    attributeString = string.Empty;
                    return attributeString;
                }
            }

            bool addQuotes = !CanRemoveAttributeQuotes(tagFlags, attributeValue, _settings.AttributeQuotesRemovalMode);
            attributeString = InnerBuildAttributeString(attributeName, attributeValue, addQuotes);

            return attributeString;
        }
        /// <summary>
        /// Builds a attribute view model
        /// </summary>
        /// <param name="context">Markup parsing context</param>
        /// <param name="tag">Tag</param>
        /// <param name="attribute">Attribute</param>
        /// <returns>String representation of the attribute</returns>
        private HtmlAttributeViewModel BuildAttributeViewModel(MarkupParsingContext context, HtmlTag tag, HtmlAttribute attribute)
        {
            string tagNameInLowercase = tag.NameInLowercase;
            HtmlAttributeViewModel attributeViewModel;
            string attributeName = attribute.Name;
            string attributeNameInLowercase = attribute.NameInLowercase;
            string attributeValue = attribute.Value;
            bool attributeHasValue = attribute.HasValue;
            bool attributeHasEmptyValue = !attributeHasValue || attributeValue.Length == 0;
            HtmlAttributeType attributeType = attribute.Type;
            bool useHtmlSyntax = !_settings.UseXhtmlSyntax;

            if (useHtmlSyntax && attributeType == HtmlAttributeType.Xml && attributeNameInLowercase != "xmlns")
            {
                string sourceCode = context.SourceCode;
                SourceCodeNodeCoordinates attributeCoordinates = attribute.NameCoordinates;

                WriteWarning(LogCategoryConstants.HtmlMinificationWarning,
                    string.Format(Strings.WarningMessage_XmlBasedAttributeNotAllowed, attributeName), _fileContext,
                    attributeCoordinates.LineNumber, attributeCoordinates.ColumnNumber,
                    SourceCodeNavigator.GetSourceFragment(sourceCode, attributeCoordinates));
            }

            if ((_settings.RemoveRedundantAttributes && IsAttributeRedundant(tag, attribute))
                || (_settings.RemoveJsTypeAttributes && IsJavaScriptTypeAttribute(tag, attribute))
                || (_settings.RemoveCssTypeAttributes && IsCssTypeAttribute(tag, attribute))
                || (useHtmlSyntax && CanRemoveXmlNamespaceAttribute(tag, attribute)))
            {
                if (CanRemoveAttribute(tag, attribute))
                {
                    attributeViewModel = HtmlAttributeViewModel.Empty;
                    return attributeViewModel;
                }
            }

            bool isCustomBooleanAttribute = !attributeHasValue && attributeType == HtmlAttributeType.Text;
            if (isCustomBooleanAttribute)
            {
                if (useHtmlSyntax)
                {
                    attributeViewModel = InnerBuildAttributeViewModel(attribute, true, false);
                    return attributeViewModel;
                }

                attribute.Value = string.Empty;
            }
            else if (attributeType != HtmlAttributeType.Event
                && !attributeHasEmptyValue
                && TemplateTagHelpers.ContainsTag(attributeValue))
            {
                // Processing of template tags
                StringBuilder attributeValueBuilder = StringBuilderPool.GetBuilder();

                TemplateTagHelpers.ParseMarkup(attributeValue,
                    (localContext, expression, startDelimiter, endDelimiter) =>
                    {
                        string processedExpression = expression;
                        if (_settings.MinifyAngularBindingExpressions && startDelimiter == "{{" && endDelimiter == "}}")
                        {
                            processedExpression = MinifyAngularBindingExpression(context, attribute.ValueCoordinates,
                                localContext.NodeCoordinates, expression);
                        }

                        attributeValueBuilder.Append(startDelimiter);
                        attributeValueBuilder.Append(processedExpression);
                        attributeValueBuilder.Append(endDelimiter);
                    },
                    (localContext, textValue) =>
                    {
                        string processedTextValue = textValue;
                        if (attributeType == HtmlAttributeType.ClassName)
                        {
                            processedTextValue = Utils.CollapseWhitespace(textValue);
                        }

                        attributeValueBuilder.Append(processedTextValue);
                    }
                );

                string processedAttributeValue = attributeValueBuilder.ToString();
                StringBuilderPool.ReleaseBuilder(attributeValueBuilder);

                switch (attributeType)
                {
                    case HtmlAttributeType.Uri:
                    case HtmlAttributeType.Numeric:
                    case HtmlAttributeType.ClassName:
                        processedAttributeValue = processedAttributeValue.Trim();
                        break;
                    case HtmlAttributeType.Style:
                        processedAttributeValue = processedAttributeValue.Trim();
                        processedAttributeValue = Utils.RemoveEndingSemicolon(processedAttributeValue);
                        break;
                    default:
                        if (_settings.MinifyAngularBindingExpressions && tag.Flags.HasFlag(HtmlTagFlags.Custom))
                        {
                            string elementDirectiveName = AngularHelpers.NormalizeDirectiveName(tagNameInLowercase);
                            if (elementDirectiveName == "ngPluralize" && attributeNameInLowercase == "when")
                            {
                                processedAttributeValue = MinifyAngularBindingExpression(context, attribute.ValueCoordinates,
                                    processedAttributeValue);
                            }
                        }

                        break;
                }

                attribute.Value = processedAttributeValue;
            }
            else if (attributeType == HtmlAttributeType.Boolean)
            {
                if (_settings.CollapseBooleanAttributes)
                {
                    attributeViewModel = InnerBuildAttributeViewModel(attribute, true, false);
                    return attributeViewModel;
                }

                attribute.Value = attributeName;
            }
            else
            {
                if (!attributeHasEmptyValue)
                {
                    attribute.Value = CleanAttributeValue(context, tag, attribute);
                }

                if (_settings.RemoveEmptyAttributes && CanRemoveEmptyAttribute(tag, attribute))
                {
                    if (CanRemoveAttribute(tag, attribute))
                    {
                        attributeViewModel = HtmlAttributeViewModel.Empty;
                        return attributeViewModel;
                    }
                }
            }

            bool addQuotes = !CanRemoveAttributeQuotes(attribute, _settings.AttributeQuotesRemovalMode);
            attributeViewModel = InnerBuildAttributeViewModel(attribute, false, addQuotes);

            return attributeViewModel;
        }
        /// <summary>
        /// Start tags handler
        /// </summary>
        /// <param name="context">Markup parsing context</param>
        /// <param name="tag">HTML tag</param>
        private void EndTagHandler(MarkupParsingContext context, HtmlTag tag)
        {
            HtmlNodeType previousNodeType = _currentNodeType;
            string previousTagName;
            IList<HtmlAttribute> previousTagAttributes;
            if (_currentTag != null)
            {
                previousTagName = _currentTag.Name;
                previousTagAttributes = _currentTag.Attributes;
            }
            else
            {
                previousTagName = string.Empty;
                previousTagAttributes = new List<HtmlAttribute>();
            }
            string previousText = _currentText;

            _currentNodeType = HtmlNodeType.EndTag;
            _currentTag = tag;
            _currentText = string.Empty;

            string tagName = tag.Name;
            HtmlTagFlags tagFlags = tag.Flags;

            WhitespaceMinificationMode whitespaceMinificationMode = _settings.WhitespaceMinificationMode;
            if (whitespaceMinificationMode != WhitespaceMinificationMode.None)
            {
                if (_tagsWithNotRemovableWhitespaceQueue.Count == 0 && !tagFlags.EmbeddedCode)
                {
                    // Processing of whitespace, that followed before the end tag
                    bool allowTrimEnd = false;
                    if (tagFlags.Invisible)
                    {
                        allowTrimEnd = true;
                    }
                    else
                    {
                        if (whitespaceMinificationMode == WhitespaceMinificationMode.Medium)
                        {
                            allowTrimEnd = tagFlags.Block;
                        }
                        else if (whitespaceMinificationMode == WhitespaceMinificationMode.Aggressive)
                        {
                            allowTrimEnd = (tagFlags.Block || tagFlags.Inline || tagFlags.InlineBlock);
                        }
                    }

                    if (allowTrimEnd)
                    {
                        TrimEndLastBufferItem();
                    }
                }

                // Check if current tag is in a whitespace queue
                if (_tagsWithNotRemovableWhitespaceQueue.Count > 0 && tagName == _tagsWithNotRemovableWhitespaceQueue.Last())
                {
                    _tagsWithNotRemovableWhitespaceQueue.Dequeue();
                }
            }

            if (_settings.RemoveOptionalEndTags
                && (previousNodeType == HtmlNodeType.EndTag
                    || (previousTagName != tagName && string.IsNullOrWhiteSpace(previousText)))
                && !IsSafeOptionalEndTag(previousTagName))
            {

                if (CanRemoveOptionalTagByParentTagName(previousTagName, tagName))
                {
                    RemoveLastEndTagFromBuffer(previousTagName);
                }
            }

            bool isElementEmpty = (string.IsNullOrWhiteSpace(previousText) && previousTagName == tagName
                && previousNodeType != HtmlNodeType.EndTag);
            if (_settings.RemoveTagsWithoutContent && isElementEmpty
                && CanRemoveTagWithoutContent(previousTagName, previousTagAttributes))
            {
                // Remove last "element" from buffer, return
                if (RemoveLastStartTagFromBuffer(tagName))
                {
                    FlushBuffer();
                    return;
                }
            }

            if (_settings.RemoveOptionalEndTags && tagFlags.OptionalEndTag && IsSafeOptionalEndTag(tagName))
            {
                // Leave only start tag in buffer
                FlushBuffer();
                return;
            }

            // Add end tag to buffer
            _buffer.Add("</");
            _buffer.Add(tagName);
            _buffer.Add(">");
        }
        /// <summary>
        /// Checks whether the attribute is redundant
        /// </summary>
        /// <param name="tag">Tag</param>
        /// <param name="attribute">Attribute</param>
        /// <returns>Result of check (true - is redundant; false - is not redundant)</returns>
        private static bool IsAttributeRedundant(HtmlTag tag, HtmlAttribute attribute)
        {
            string tagNameInLowercase = tag.NameInLowercase;
            IList<HtmlAttribute> attributes = tag.Attributes;
            string attributeNameInLowercase = attribute.NameInLowercase;
            string attributeValue = attribute.Value;
            string processedAttributeValue = attributeValue.Trim();

            return (
                (tagNameInLowercase == "script"
                    && ((attributeNameInLowercase == "language" && processedAttributeValue.IgnoreCaseEquals("javascript"))
                    || (attributeNameInLowercase == "charset" && attributes.All(a => a.NameInLowercase != "src"))))
                || (tagNameInLowercase == "link" && attributeNameInLowercase == "charset" && attributes.Any(
                    a => a.NameInLowercase == "rel" && a.Value.Trim().IgnoreCaseEquals("stylesheet")))
                || (tagNameInLowercase == "form" && attributeNameInLowercase == "method"
                    && processedAttributeValue.IgnoreCaseEquals("get"))
                || (tagNameInLowercase == "input" && attributeNameInLowercase == "type"
                    && processedAttributeValue.IgnoreCaseEquals("text"))
                || (tagNameInLowercase == "a" && attributeNameInLowercase == "name" && attributes.Any(
                    a => a.NameInLowercase == "id" && a.Value == attributeValue))
                || (tagNameInLowercase == "area" && attributeNameInLowercase == "shape"
                    && processedAttributeValue.IgnoreCaseEquals("rect"))
            );
        }
 /// <summary>
 /// Checks whether attribute is the attribute <code>type</code> of
 /// tag <code>script</code>, that containing JavaScript code
 /// </summary>
 /// <param name="tag">Tag</param>
 /// <param name="attribute">Attribute</param>
 /// <returns>Result of check</returns>
 private static bool IsJavaScriptTypeAttribute(HtmlTag tag, HtmlAttribute attribute)
 {
     return tag.NameInLowercase == "script" && attribute.NameInLowercase == "type"
         && attribute.Value.Trim().IgnoreCaseEquals(JS_CONTENT_TYPE);
 }
        /// <summary>
        /// Removes a last end tag from the HTML minification buffer
        /// </summary>
        /// <param name="endTag">End tag</param>
        private void RemoveLastEndTagFromBuffer(HtmlTag endTag)
        {
            int bufferItemCount = _buffer.Count;
            if (bufferItemCount == 0)
            {
                return;
            }

            int lastEndTagBeginAngleBracketIndex = _buffer.LastIndexOf("</");

            if (lastEndTagBeginAngleBracketIndex != -1)
            {
                string lastEndTagName = _buffer[lastEndTagBeginAngleBracketIndex + 1];
                if (lastEndTagName.IgnoreCaseEquals(endTag.NameInLowercase))
                {
                    int lastEndTagEndAngleBracketIndex = _buffer.IndexOf(">", lastEndTagBeginAngleBracketIndex);
                    if (lastEndTagEndAngleBracketIndex != -1)
                    {
                        int lastBufferItemIndex = bufferItemCount - 1;
                        bool noMoreContent = true;
                        if (lastEndTagEndAngleBracketIndex != lastBufferItemIndex)
                        {
                            for (int bufferItemIndex = lastEndTagEndAngleBracketIndex + 1;
                                bufferItemIndex < bufferItemCount;
                                bufferItemIndex++)
                            {
                                if (!string.IsNullOrWhiteSpace(_buffer[bufferItemIndex]))
                                {
                                    noMoreContent = false;
                                    break;
                                }
                            }
                        }

                        if (noMoreContent)
                        {
                            int endTagLength = lastEndTagEndAngleBracketIndex - lastEndTagBeginAngleBracketIndex + 1;
                            _buffer.RemoveRange(lastEndTagBeginAngleBracketIndex, endTagLength);
                        }
                    }
                }
            }
        }
        /// <summary>
        /// Checks whether remove whitespace between non-independent tags
        /// </summary>
        /// <param name="firstTag">First tag</param>
        /// <param name="secondTag">Second tag</param>
        /// <returns>Result of check (true - can be removed; false - can not be removed)</returns>
        private static bool CanRemoveWhitespaceBetweenNonIndependentTags(HtmlTag firstTag, HtmlTag secondTag)
        {
            string firstTagNameInLowercase = firstTag.NameInLowercase;
            string secondTagNameInLowercase = secondTag.NameInLowercase;
            bool cannotRemove;

            switch (firstTagNameInLowercase)
            {
                case "li":
                    cannotRemove = secondTagNameInLowercase == "li";
                    break;
                case "dt":
                case "dd":
                    cannotRemove = secondTagNameInLowercase == "dt" || secondTagNameInLowercase == "dd";
                    break;
                case "img":
                    cannotRemove = secondTagNameInLowercase == "figcaption";
                    break;
                default:
                    cannotRemove = secondTagNameInLowercase == "rt" || secondTagNameInLowercase == "rp"
                        || secondTagNameInLowercase == "rb" || secondTagNameInLowercase == "rtc";
                    break;
            }

            return !cannotRemove;
        }
        /// <summary>
        /// Checks whether remove an the safe optional end tag
        /// </summary>
        /// <param name="optionalEndTag">Optional end tag</param>
        /// <returns>Result of check (true - can be removed; false - can not be removed)</returns>
        private bool CanRemoveSafeOptionalEndTag(HtmlTag optionalEndTag)
        {
            string optionalEndTagNameInLowercase = optionalEndTag.NameInLowercase;
            if (_settings.PreservableOptionalTagCollection.Contains(optionalEndTagNameInLowercase))
            {
                return false;
            }

            return _safeOptionalEndTags.Contains(optionalEndTagNameInLowercase);
        }
        /// <summary>
        /// Cleans a attribute value
        /// </summary>
        /// <param name="context">Markup parsing context</param>
        /// <param name="tag">Tag</param>
        /// <param name="attribute">Attribute</param>
        /// <returns>Processed attribute value</returns>
        private string CleanAttributeValue(MarkupParsingContext context, HtmlTag tag, HtmlAttribute attribute)
        {
            string attributeValue = attribute.Value;
            if (attributeValue.Length == 0)
            {
                return attributeValue;
            }

            string processedAttributeValue = attributeValue;
            string tagNameInLowercase = tag.NameInLowercase;
            IList<HtmlAttribute> attributes = tag.Attributes;
            string attributeNameInLowercase = attribute.NameInLowercase;
            HtmlAttributeType attributeType = attribute.Type;

            switch (attributeType)
            {
                case HtmlAttributeType.Uri:
                    processedAttributeValue = processedAttributeValue.Trim();

                    if (processedAttributeValue.StartsWith(HTTP_PROTOCOL, StringComparison.OrdinalIgnoreCase))
                    {
                        if (_settings.RemoveHttpProtocolFromAttributes && !ContainsRelExternalAttribute(attributes))
                        {
                            int httpProtocolLength = HTTP_PROTOCOL.Length;
                            processedAttributeValue = processedAttributeValue.Substring(httpProtocolLength);
                        }
                    }
                    else if (processedAttributeValue.StartsWith(HTTPS_PROTOCOL, StringComparison.OrdinalIgnoreCase))
                    {
                        if (_settings.RemoveHttpsProtocolFromAttributes && !ContainsRelExternalAttribute(attributes))
                        {
                            int httpsProtocolLength = HTTPS_PROTOCOL.Length;
                            processedAttributeValue = processedAttributeValue.Substring(httpsProtocolLength);
                        }
                    }
                    else if (attributeNameInLowercase == "href"
                        && processedAttributeValue.StartsWith(JS_PROTOCOL, StringComparison.OrdinalIgnoreCase))
                    {
                        processedAttributeValue = ProcessInlineScriptContent(context, attribute);
                    }

                    break;
                case HtmlAttributeType.Numeric:
                    processedAttributeValue = processedAttributeValue.Trim();
                    break;
                case HtmlAttributeType.ClassName:
                    if (AngularHelpers.IsClassDirective(processedAttributeValue))
                    {
                        // Processing of Angular class directives
                        string ngOriginalDirectiveName = string.Empty;
                        string ngNormalizedDirectiveName = string.Empty;
                        string ngExpression;
                        var ngDirectives = new Dictionary<string, string>();

                        AngularHelpers.ParseClassDirective(processedAttributeValue,
                            (localContext, originalDirectiveName, normalizedDirectiveName) =>
                            {
                                ngOriginalDirectiveName = originalDirectiveName;
                                ngNormalizedDirectiveName = normalizedDirectiveName;
                                ngExpression = null;

                                ngDirectives.Add(ngOriginalDirectiveName, ngExpression);
                            },
                            (localContext, expression) =>
                            {
                                ngExpression = expression;
                                if (_settings.MinifyAngularBindingExpressions
                                    && ContainsAngularBindingExpression(ngNormalizedDirectiveName))
                                {
                                    ngExpression = MinifyAngularBindingExpression(context,
                                        attribute.ValueCoordinates, localContext.NodeCoordinates,
                                        expression);
                                }

                                ngDirectives[ngOriginalDirectiveName] = ngExpression;
                            },
                            localContext =>
                            {
                                if (ngDirectives[ngOriginalDirectiveName] == null)
                                {
                                    ngDirectives[ngOriginalDirectiveName] = string.Empty;
                                }
                            }
                        );

                        int directiveCount = ngDirectives.Count;
                        if (directiveCount > 0)
                        {
                            StringBuilder directiveBuilder = StringBuilderPool.GetBuilder();
                            int directiveIndex = 0;
                            int lastDirectiveIndex = directiveCount - 1;
                            string previousExpression = null;

                            foreach (var directive in ngDirectives)
                            {
                                string directiveName = directive.Key;
                                string expression = directive.Value;

                                if (directiveIndex > 0 && (expression == null || previousExpression == null))
                                {
                                    directiveBuilder.Append(" ");
                                }

                                directiveBuilder.Append(directiveName);
                                if (!string.IsNullOrWhiteSpace(expression))
                                {
                                    directiveBuilder.AppendFormat(":{0}", expression);
                                }

                                if (directiveIndex < lastDirectiveIndex && expression != null)
                                {
                                    directiveBuilder.Append(";");
                                }

                                previousExpression = expression;
                                directiveIndex++;
                            }

                            processedAttributeValue = directiveBuilder.ToString();
                            StringBuilderPool.ReleaseBuilder(directiveBuilder);
                        }
                        else
                        {
                            processedAttributeValue = string.Empty;
                        }
                    }
                    else
                    {
                        processedAttributeValue = processedAttributeValue.Trim();
                        processedAttributeValue = Utils.CollapseWhitespace(processedAttributeValue);
                    }

                    break;
                case HtmlAttributeType.Style:
                    processedAttributeValue = ProcessInlineStyleContent(context, attribute);
                    break;
                case HtmlAttributeType.Event:
                    processedAttributeValue = ProcessInlineScriptContent(context, attribute);
                    break;
                default:
                    if (attributeNameInLowercase == "data-bind" && _settings.MinifyKnockoutBindingExpressions)
                    {
                        processedAttributeValue = MinifyKnockoutBindingExpression(context, attribute);
                    }
                    else if (tagNameInLowercase == "meta" && attributeNameInLowercase == "content"
                        && attributes.Any(a => a.NameInLowercase == "name" && a.Value.Trim().IgnoreCaseEquals("keywords")))
                    {
                        processedAttributeValue = processedAttributeValue.Trim();
                        processedAttributeValue = Utils.CollapseWhitespace(processedAttributeValue);
                        processedAttributeValue = _separatingCommaWithSpacesRegex.Replace(processedAttributeValue, ",");
                        processedAttributeValue = _endingCommaWithSpacesRegex.Replace(processedAttributeValue, string.Empty);
                    }
                    else
                    {
                        if (_settings.MinifyAngularBindingExpressions
                            && CanMinifyAngularBindingExpressionInAttribute(tag, attribute))
                        {
                            processedAttributeValue = MinifyAngularBindingExpression(context, attribute.ValueCoordinates,
                                processedAttributeValue);
                        }
                    }

                    break;
            }

            return processedAttributeValue;
        }
        /// <summary>
        /// Checks whether remove an the optional end tag
        /// </summary>
        /// <param name="optionalEndTag">Optional end tag</param>
        /// <param name="parentTag">Parent tag</param>
        /// <returns>Result of check (true - can be removed; false - can not be removed)</returns>
        private bool CanRemoveOptionalEndTagByParentTag(HtmlTag optionalEndTag, HtmlTag parentTag)
        {
            string optionalEndTagNameInLowercase = optionalEndTag.NameInLowercase;
            if (_settings.PreservableOptionalTagCollection.Contains(optionalEndTagNameInLowercase))
            {
                return false;
            }

            string parentTagNameInLowercase = parentTag.NameInLowercase;
            bool canRemove;

            switch (optionalEndTagNameInLowercase)
            {
                case "p":
                    canRemove = parentTagNameInLowercase != "a";
                    break;
                case "li":
                    canRemove = parentTagNameInLowercase == "ul" || parentTagNameInLowercase == "ol"
                        || parentTagNameInLowercase == "menu";
                    break;
                case "tbody":
                case "tfoot":
                    canRemove = parentTagNameInLowercase == "table";
                    break;
                case "tr":
                    canRemove = parentTagNameInLowercase == "table" || parentTagNameInLowercase == "thead"
                        || parentTagNameInLowercase == "tbody" || parentTagNameInLowercase == "tfoot";
                    break;
                case "td":
                case "th":
                    canRemove = parentTagNameInLowercase == "tr";
                    break;
                case "option":
                    canRemove = parentTagNameInLowercase == "select" || parentTagNameInLowercase == "optgroup"
                        || parentTagNameInLowercase == "datalist";
                    break;
                case "optgroup":
                    canRemove = parentTagNameInLowercase == "select";
                    break;
                case "dd":
                    canRemove = parentTagNameInLowercase == "dl";
                    break;
                case "rt":
                    canRemove = parentTagNameInLowercase == "ruby" || parentTagNameInLowercase == "rtc";
                    break;
                case "rp":
                case "rb":
                case "rtc":
                    canRemove = parentTagNameInLowercase == "ruby";
                    break;
                default:
                    canRemove = false;
                    break;
            }

            return canRemove;
        }
        /// <summary>
        /// Checks whether remove an the optional end tag
        /// </summary>
        /// <param name="optionalEndTag">Optional end tag</param>
        /// <param name="nextTag">Next tag</param>
        /// <returns>Result of check (true - can be removed; false - can not be removed)</returns>
        private bool CanRemoveOptionalEndTagByNextTag(HtmlTag optionalEndTag, HtmlTag nextTag)
        {
            string optionalEndTagNameInLowercase = optionalEndTag.NameInLowercase;
            if (_settings.PreservableOptionalTagCollection.Contains(optionalEndTagNameInLowercase))
            {
                return false;
            }

            string nextTagNameInLowercase = nextTag.NameInLowercase;
            bool canRemove;

            switch (optionalEndTagNameInLowercase)
            {
                case "p":
                    canRemove = _tagsFollowingAfterParagraphOptionalEndTag.Contains(nextTagNameInLowercase);
                    break;
                case "li":
                    canRemove = nextTagNameInLowercase == "li";
                    break;
                case "thead":
                case "tbody":
                    canRemove = nextTagNameInLowercase == "tbody" || nextTagNameInLowercase == "tfoot";
                    break;
                case "tfoot":
                    canRemove = nextTagNameInLowercase == "tbody";
                    break;
                case "tr":
                    canRemove = nextTagNameInLowercase == "tr";
                    break;
                case "td":
                case "th":
                    canRemove = nextTagNameInLowercase == "td" || nextTagNameInLowercase == "th";
                    break;
                case "option":
                    canRemove = nextTagNameInLowercase == "option" || nextTagNameInLowercase == "optgroup";
                    break;
                case "optgroup":
                    canRemove = nextTagNameInLowercase == "optgroup";
                    break;
                case "dt":
                case "dd":
                    canRemove = nextTagNameInLowercase == "dt" || nextTagNameInLowercase == "dd";
                    break;
                case "rt":
                case "rp":
                case "rb":
                    canRemove = nextTagNameInLowercase == "rt" || nextTagNameInLowercase == "rp"
                        || nextTagNameInLowercase == "rb" || nextTagNameInLowercase == "rtc"
                        ;
                    break;
                case "rtc":
                    canRemove = nextTagNameInLowercase == "rp" || nextTagNameInLowercase == "rb"
                        || nextTagNameInLowercase == "rtc"
                        ;
                    break;
                default:
                    canRemove = false;
                    break;
            }

            return canRemove;
        }
Example #13
0
        /// <summary>
        /// Parses a end tag
        /// </summary>
        /// <param name="tagName">Tag name</param>
        /// <param name="tagNameInLowercase">Tag name in lowercase</param>
        private void ParseEndTag(string tagName, string tagNameInLowercase)
        {
            int  endTagIndex     = 0;
            int  lastTagIndex    = _tagStack.Count - 1;
            bool tagNameNotEmpty = !string.IsNullOrEmpty(tagName);

            HtmlParsingHandlers.EndTagDelegate endTagHandler = _handlers.EndTag;

            if (tagNameNotEmpty)
            {
                for (endTagIndex = lastTagIndex; endTagIndex >= 0; endTagIndex--)
                {
                    if (_tagStack[endTagIndex].NameInLowercase == tagNameInLowercase)
                    {
                        break;
                    }
                }
            }

            if (endTagIndex >= 0)
            {
                // Close all the open elements, up the stack
                if (endTagHandler != null)
                {
                    for (int tagIndex = lastTagIndex; tagIndex >= endTagIndex; tagIndex--)
                    {
                        HtmlTag      startTag = _tagStack[tagIndex];
                        string       startTagNameInLowercase = startTag.NameInLowercase;
                        HtmlTagFlags startTagFlags           = startTag.Flags;

                        string endTagName;
                        if (tagNameNotEmpty && tagNameInLowercase == startTagNameInLowercase)
                        {
                            endTagName = tagName;
                        }
                        else
                        {
                            endTagName = startTag.Name;
                        }

                        if (_xmlTagStack.Count > 0 && !startTagFlags.IsSet(HtmlTagFlags.NonIndependent))
                        {
                            _xmlTagStack.Pop();
                        }

                        var endTag = new HtmlTag(endTagName, startTagNameInLowercase, startTagFlags);
                        endTagHandler(_context, endTag);
                    }
                }

                // Remove the open elements from the stack
                if (endTagIndex <= lastTagIndex)
                {
                    int tagToRemoveStartIndex = endTagIndex;
                    int tagsToRemoveCount     = lastTagIndex - endTagIndex + 1;

                    _tagStack.RemoveRange(tagToRemoveStartIndex, tagsToRemoveCount);
                }
            }
            else if (tagNameNotEmpty && _conditionalCommentOpened)
            {
                if (_xmlTagStack.Count > 0 && _tagTypeDeterminer.IsXmlBasedTag(tagNameInLowercase))
                {
                    _xmlTagStack.Pop();
                }

                var endTag = new HtmlTag(tagName, tagNameInLowercase, GetTagFlagsByName(tagNameInLowercase));
                endTagHandler?.Invoke(_context, endTag);
            }
        }
Example #14
0
        /// <summary>
        /// Parses a end tag
        /// </summary>
        /// <param name="tagName">Tag name</param>
        /// <param name="tagNameInLowercase">Tag name in lowercase</param>
        private void ParseEndTag(string tagName, string tagNameInLowercase)
        {
            int endTagIndex = 0;
            int lastTagIndex = _tagStack.Count - 1;
            bool tagNameNotEmpty = !string.IsNullOrEmpty(tagName);
            HtmlParsingHandlers.EndTagDelegate endTagHandler = _handlers.EndTag;

            if (tagNameNotEmpty)
            {
                for (endTagIndex = lastTagIndex; endTagIndex >= 0; endTagIndex--)
                {
                    if (_tagStack[endTagIndex].NameInLowercase == tagNameInLowercase)
                    {
                        break;
                    }
                }
            }

            if (endTagIndex >= 0)
            {
                // Close all the open elements, up the stack
                if (endTagHandler != null)
                {
                    for (int tagIndex = lastTagIndex; tagIndex >= endTagIndex; tagIndex--)
                    {
                        HtmlTag startTag = _tagStack[tagIndex];
                        string startTagNameInLowercase = startTag.NameInLowercase;
                        HtmlTagFlags startTagFlags = startTag.Flags;

                        string endTagName;
                        if (tagNameNotEmpty && tagNameInLowercase == startTagNameInLowercase)
                        {
                            endTagName = tagName;
                        }
                        else
                        {
                            endTagName = startTag.Name;
                        }

                        if (_xmlTagStack.Count > 0 && !startTagFlags.HasFlag(HtmlTagFlags.NonIndependent))
                        {
                            _xmlTagStack.Pop();
                        }

                        var endTag = new HtmlTag(endTagName, startTagNameInLowercase, startTagFlags);
                        endTagHandler(_context, endTag);
                    }
                }

                // Remove the open elements from the stack
                if (endTagIndex <= lastTagIndex)
                {
                    int tagToRemoveStartIndex = endTagIndex;
                    int tagsToRemoveCount = lastTagIndex - endTagIndex + 1;

                    _tagStack.RemoveRange(tagToRemoveStartIndex, tagsToRemoveCount);
                }
            }
            else if (tagNameNotEmpty && _conditionalCommentOpened)
            {
                if (_xmlTagStack.Count > 0 && HtmlTagFlagsHelpers.IsXmlBasedTag(tagNameInLowercase))
                {
                    _xmlTagStack.Pop();
                }

                var endTag = new HtmlTag(tagName, tagNameInLowercase, GetTagFlagsByName(tagNameInLowercase));
                if (endTagHandler != null)
                {
                    endTagHandler(_context, endTag);
                }
            }
        }
        /// <summary>
        /// Removes a last start tag from the HTML minification buffer
        /// </summary>
        /// <param name="startTag">Start tag</param>
        /// <returns>Result of removing (true - has removed; false - has not removed)</returns>
        private bool RemoveLastStartTagFromBuffer(HtmlTag startTag)
        {
            int bufferItemCount = _buffer.Count;
            if (bufferItemCount == 0)
            {
                return false;
            }

            bool isEndTagRemoved = false;
            int lastStartTagBeginAngleBracketIndex = _buffer.LastIndexOf("<");

            if (lastStartTagBeginAngleBracketIndex != -1)
            {
                string lastTagName = _buffer[lastStartTagBeginAngleBracketIndex + 1];
                if (lastTagName.IgnoreCaseEquals(startTag.NameInLowercase))
                {
                    int lastStartTagEndAngleBracketIndex = _buffer.IndexOf(">", lastStartTagBeginAngleBracketIndex);
                    if (lastStartTagEndAngleBracketIndex != -1)
                    {
                        int lastBufferItemIndex = bufferItemCount - 1;
                        bool noMoreContent = true;
                        if (lastStartTagEndAngleBracketIndex != lastBufferItemIndex)
                        {
                            for (int bufferItemIndex = lastStartTagEndAngleBracketIndex + 1;
                                 bufferItemIndex < bufferItemCount;
                                 bufferItemIndex++)
                            {
                                if (!string.IsNullOrWhiteSpace(_buffer[bufferItemIndex]))
                                {
                                    noMoreContent = false;
                                    break;
                                }
                            }
                        }

                        if (noMoreContent)
                        {
                            _buffer.RemoveRange(lastStartTagBeginAngleBracketIndex,
                                bufferItemCount - lastStartTagBeginAngleBracketIndex);

                            isEndTagRemoved = true;
                        }
                    }
                }
            }

            return isEndTagRemoved;
        }
Example #16
0
        /// <summary>
        /// Parses a start tag
        /// </summary>
        /// <param name="tagName">Tag name</param>
        /// <param name="tagNameInLowercase">Tag name in lowercase</param>
        /// <param name="attributes">List of attributes</param>
        /// <param name="isEmptyTag">Flag that tag is empty</param>
        private void ParseStartTag(string tagName, string tagNameInLowercase, List <HtmlAttribute> attributes,
                                   bool isEmptyTag)
        {
            HtmlTagFlags tagFlags = GetTagFlagsByName(tagNameInLowercase);

            if (tagFlags.IsSet(HtmlTagFlags.Optional))
            {
                HtmlTag lastStackedTag = _tagStack.LastOrDefault();
                if (lastStackedTag != null && lastStackedTag.NameInLowercase == tagNameInLowercase)
                {
                    ParseEndTag(lastStackedTag.Name, lastStackedTag.NameInLowercase);
                }
                else
                {
                    if (tagNameInLowercase == "body" && _tagStack.Any(t => t.NameInLowercase == "head"))
                    {
                        HtmlTag headTag = _tagStack.First(t => t.NameInLowercase == "head");
                        ParseEndTag(headTag.Name, headTag.NameInLowercase);
                    }
                }
            }

            if (tagFlags.IsSet(HtmlTagFlags.Empty))
            {
                isEmptyTag = true;
            }
            else if (isEmptyTag)
            {
                tagFlags |= HtmlTagFlags.Empty;
            }

            int attributeCount = attributes.Count;

            for (int attributeIndex = 0; attributeIndex < attributeCount; attributeIndex++)
            {
                HtmlAttribute attribute = attributes[attributeIndex];
                attribute.Type = _attributeTypeDeterminer.GetAttributeType(tagNameInLowercase, tagFlags,
                                                                           attribute.NameInLowercase, attributes);
            }

            var tag = new HtmlTag(tagName, tagNameInLowercase, attributes, tagFlags);

            if (!isEmptyTag)
            {
                if (_conditionalCommentOpened)
                {
                    HtmlConditionalComment     lastConditionalComment     = _conditionalCommentStack.Peek();
                    HtmlConditionalCommentType lastConditionalCommentType = lastConditionalComment.Type;

                    if (tagFlags.IsSet(HtmlTagFlags.EmbeddedCode) ||
                        lastConditionalCommentType == HtmlConditionalCommentType.RevealedValidating ||
                        lastConditionalCommentType == HtmlConditionalCommentType.RevealedValidatingSimplified)
                    {
                        _tagStack.Add(tag);
                    }
                }
                else
                {
                    _tagStack.Add(tag);
                }
            }

            _handlers.StartTag?.Invoke(_context, tag);

            if (tagFlags.IsSet(HtmlTagFlags.Xml) && !tagFlags.IsSet(HtmlTagFlags.NonIndependent))
            {
                _xmlTagStack.Push(tagNameInLowercase);
            }
        }
        /// <summary>
        /// Start tags handler
        /// </summary>
        /// <param name="context">Markup parsing context</param>
        /// <param name="tag">HTML tag</param>
        private void StartTagHandler(MarkupParsingContext context, HtmlTag tag)
        {
            HtmlNodeType previousNodeType = _currentNodeType;
            HtmlTag previousTag = _currentTag ?? HtmlTag.Empty;

            if (_settings.UseMetaCharsetTag && IsMetaContentTypeTag(tag))
            {
                tag = UpgradeToMetaCharsetTag(tag);
            }

            string tagName = tag.Name;
            string tagNameInLowercase = tag.NameInLowercase;
            HtmlTagFlags tagFlags = tag.Flags;
            IList<HtmlAttribute> attributes = tag.Attributes;

            _currentNodeType = HtmlNodeType.StartTag;
            _currentTag = tag;
            _currentText = string.Empty;

            // Set whitespace flags for nested tags (for example <span> within a <pre>)
            WhitespaceMinificationMode whitespaceMinificationMode = _settings.WhitespaceMinificationMode;
            if (whitespaceMinificationMode != WhitespaceMinificationMode.None)
            {
                if (_tagsWithNotRemovableWhitespaceQueue.Count == 0)
                {
                    // Processing of whitespace, that followed before the start tag
                    bool allowTrimEnd = false;
                    if (tagFlags.HasFlag(HtmlTagFlags.Invisible)
                        || (tagFlags.HasFlag(HtmlTagFlags.NonIndependent)
                            && CanRemoveWhitespaceBetweenNonIndependentTags(previousTag, tag)))
                    {
                        allowTrimEnd = true;
                    }
                    else
                    {
                        if (whitespaceMinificationMode == WhitespaceMinificationMode.Medium
                            || whitespaceMinificationMode == WhitespaceMinificationMode.Aggressive)
                        {
                            allowTrimEnd = tagFlags.HasFlag(HtmlTagFlags.Block);
                        }
                    }

                    if (allowTrimEnd)
                    {
                        TrimEndLastBufferItem();
                    }
                }

                if (!CanMinifyWhitespace(tag))
                {
                    _tagsWithNotRemovableWhitespaceQueue.Enqueue(tagNameInLowercase);
                }
            }

            if (previousNodeType != HtmlNodeType.StartTag)
            {
                if (_settings.RemoveOptionalEndTags
                    && previousTag.Flags.HasFlag(HtmlTagFlags.Optional)
                    && CanRemoveOptionalEndTagByNextTag(previousTag, tag))
                {
                    RemoveLastEndTagFromBuffer(previousTag);
                }

                FlushBuffer();
            }

            _buffer.Add("<");
            _buffer.Add(CanPreserveCase() ? tagName : tagNameInLowercase);

            int attributeCount = attributes.Count;
            bool unsafeLastAttribute = false;

            if (attributeCount > 0)
            {
                for (int attributeIndex = 0; attributeIndex < attributeCount; attributeIndex++)
                {
                    HtmlAttributeViewModel attributeViewModel = BuildAttributeViewModel(context, tag, attributes[attributeIndex]);
                    if (!attributeViewModel.IsEmpty)
                    {
                        _buffer.Add(" ");
                        _buffer.Add(attributeViewModel.Name);
                        if (attributeViewModel.HasValue)
                        {
                            _buffer.Add("=");
                            if (attributeViewModel.HasQuotes)
                            {
                                _buffer.Add("\"");
                            }
                            _buffer.Add(attributeViewModel.Value);
                            if (attributeViewModel.HasQuotes)
                            {
                                _buffer.Add("\"");
                            }
                        }

                        unsafeLastAttribute = attributeViewModel.HasValue && !attributeViewModel.HasQuotes;
                    }
                }
            }

            if (tagFlags.HasFlag(HtmlTagFlags.Empty))
            {
                HtmlEmptyTagRenderMode emptyTagRenderMode = _settings.EmptyTagRenderMode;

                if (emptyTagRenderMode == HtmlEmptyTagRenderMode.NoSlash && tagFlags.HasFlag(HtmlTagFlags.Xml))
                {
                    emptyTagRenderMode = HtmlEmptyTagRenderMode.SpaceAndSlash;
                }

                if (emptyTagRenderMode == HtmlEmptyTagRenderMode.Slash && unsafeLastAttribute)
                {
                    emptyTagRenderMode = HtmlEmptyTagRenderMode.SpaceAndSlash;
                }

                if (emptyTagRenderMode == HtmlEmptyTagRenderMode.Slash)
                {
                    _buffer.Add("/");
                }
                else if (emptyTagRenderMode == HtmlEmptyTagRenderMode.SpaceAndSlash)
                {
                    _buffer.Add(" /");
                }
            }
            _buffer.Add(">");
        }
 /// <summary>
 /// Checks whether remove an the <code>xmlns</code> attribute
 /// </summary>
 /// <param name="tag">Tag</param>
 /// <param name="attribute">Attribute</param>
 /// <returns>Result of check (true - can be removed; false - can not be removed)</returns>
 private static bool CanRemoveXmlNamespaceAttribute(HtmlTag tag, HtmlAttribute attribute)
 {
     return tag.NameInLowercase == "html" && attribute.NameInLowercase == "xmlns";
 }
        /// <summary>
        /// Constructs instance of generic HTML minifier
        /// </summary>
        /// <param name="settings">Generic HTML minification settings</param>
        /// <param name="cssMinifier">CSS minifier</param>
        /// <param name="jsMinifier">JS minifier</param>
        /// <param name="logger">Logger</param>
        public GenericHtmlMinifier(GenericHtmlMinificationSettings settings = null,
			ICssMinifier cssMinifier = null, IJsMinifier jsMinifier = null, ILogger logger = null)
        {
            _settings = settings ?? new GenericHtmlMinificationSettings();
            _logger = logger ?? new NullLogger();
            _cssMinifier = cssMinifier ?? new KristensenCssMinifier();
            _jsMinifier = jsMinifier ?? new CrockfordJsMinifier();
            _htmlParser = new HtmlParser(new HtmlParsingHandlers
            {
                XmlDeclaration = XmlDeclarationHandler,
                Doctype = DoctypeHandler,
                Comment = CommentHandler,
                IfConditionalComment = IfConditionalCommentHandler,
                EndIfConditionalComment = EndIfConditionalCommentHandler,
                StartTag = StartTagHandler,
                EndTag = EndTagHandler,
                Text = TextHandler,
                EmbeddedCode = EmbeddedCodeHandler,
                TemplateTag = TemplateTagHandler,
                IgnoredFragment = IgnoredFragmentHandler
            });

            _buffer = new List<string>();
            _errors = new List<MinificationErrorInfo>();
            _warnings = new List<MinificationErrorInfo>();
            _tagsWithNotRemovableWhitespaceQueue = new Queue<string>();
            _currentNodeType = HtmlNodeType.Unknown;
            _currentTag = null;
            _currentText = string.Empty;

            ISet<string> customAngularDirectivesWithExpressions = _settings.CustomAngularDirectiveCollection;
            _angularDirectivesWithExpressions = customAngularDirectivesWithExpressions.Count > 0 ?
                Utils.UnionHashSets(_builtinAngularDirectivesWithExpressions, customAngularDirectivesWithExpressions)
                :
                _builtinAngularDirectivesWithExpressions
                ;
        }
        /// <summary>
        /// Checks whether attribute is the attribute <code>type</code> of tag <code>link</code>
        /// or <code>style</code>, that containing CSS code
        /// </summary>
        /// <param name="tag">Tag</param>
        /// <param name="attribute">Attribute</param>
        /// <returns>Result of check</returns>
        private static bool IsCssTypeAttribute(HtmlTag tag, HtmlAttribute attribute)
        {
            string tagNameInLowercase = tag.NameInLowercase;
            string attributeNameInLowercase = attribute.NameInLowercase;
            string attributeValue = attribute.Value;
            IList<HtmlAttribute> attributes = tag.Attributes;
            bool isCssTypeAttribute = false;

            if (tagNameInLowercase == "link" || tagNameInLowercase == "style")
            {
                string processedAttributeValue = attributeValue.Trim();

                if (attributeNameInLowercase == "type" && processedAttributeValue.IgnoreCaseEquals(CSS_CONTENT_TYPE))
                {
                    if (tagNameInLowercase == "link")
                    {
                        isCssTypeAttribute = attributes.Any(a => a.NameInLowercase == "rel"
                            && a.Value.Trim().IgnoreCaseEquals("stylesheet"));
                    }
                    else if (tagNameInLowercase == "style")
                    {
                        isCssTypeAttribute = true;
                    }
                }
            }

            return isCssTypeAttribute;
        }
        /// <summary>
        /// Minify HTML content
        /// </summary>
        /// <param name="content">HTML content</param>
        /// <param name="fileContext">File context</param>
        /// <param name="encoding">Text encoding</param>
        /// <param name="generateStatistics">Flag for whether to allow generate minification statistics</param>
        /// <returns>Minification result</returns>
        public MarkupMinificationResult Minify(string content, string fileContext, Encoding encoding,
			bool generateStatistics)
        {
            MinificationStatistics statistics = null;
            string cleanedContent = Utils.RemoveByteOrderMark(content);
            string minifiedContent = string.Empty;
            var errors = new List<MinificationErrorInfo>();
            var warnings = new List<MinificationErrorInfo>();

            lock (_minificationSynchronizer)
            {
                _fileContext = fileContext;
                _encoding = encoding;

                try
                {
                    if (generateStatistics)
                    {
                        statistics = new MinificationStatistics(_encoding);
                        statistics.Init(cleanedContent);
                    }

                    int estimatedCapacity = (int)Math.Floor(cleanedContent.Length * AVERAGE_COMPRESSION_RATIO);
                    _result = StringBuilderPool.GetBuilder(estimatedCapacity);

                    _htmlParser.Parse(cleanedContent);

                    FlushBuffer();

                    if (_errors.Count == 0)
                    {
                        minifiedContent = _result.ToString();

                        if (generateStatistics)
                        {
                            statistics.End(minifiedContent);
                        }
                    }
                }
                catch (MarkupParsingException e)
                {
                    WriteError(LogCategoryConstants.HtmlParsingError, e.Message, _fileContext,
                        e.LineNumber, e.ColumnNumber, e.SourceFragment);
                }
                finally
                {
                    StringBuilderPool.ReleaseBuilder(_result);
                    _buffer.Clear();
                    _tagsWithNotRemovableWhitespaceQueue.Clear();
                    _currentNodeType = HtmlNodeType.Unknown;
                    _currentTag = null;
                    _currentText = string.Empty;

                    errors.AddRange(_errors);
                    warnings.AddRange(_warnings);

                    _errors.Clear();
                    _warnings.Clear();
                    _fileContext = null;
                    _encoding = null;
                }
            }

            if (errors.Count == 0)
            {
                _logger.Info(LogCategoryConstants.HtmlMinificationSuccess,
                    string.Format(Strings.SuccesMessage_MarkupMinificationComplete, "HTML"),
                    fileContext, statistics);
            }

            return new MarkupMinificationResult(minifiedContent, errors, warnings, statistics);
        }
 /// <summary>
 /// Checks whether the tag is a META content-type tag
 /// </summary>
 /// <param name="tag">Tag</param>
 /// <returns>Result of check (true - is META content-type tag; false - is not META content-type tag)</returns>
 private static bool IsMetaContentTypeTag(HtmlTag tag)
 {
     return tag.NameInLowercase == "meta" && tag.Attributes.Any(
         a => a.NameInLowercase == "http-equiv" && a.Value.Trim().IgnoreCaseEquals("content-type"));
 }
 /// <summary>
 /// Checks whether to minify whitespaces in text content of tag
 /// </summary>
 /// <param name="tag">Tag</param>
 /// <returns>Result of check (true - can minify whitespaces; false - can not minify whitespaces)</returns>
 private static bool CanMinifyWhitespace(HtmlTag tag)
 {
     return !_tagsWithNotRemovableWhitespace.Contains(tag.NameInLowercase);
 }
        /// <summary>
        /// Checks whether to minify the Angular binding expression in attribute
        /// </summary>
        /// <param name="tag">Tag</param>
        /// <param name="attribute">Attribute</param>
        /// <returns>Result of check (true - can minify expression; false - can not minify expression)</returns>
        private bool CanMinifyAngularBindingExpressionInAttribute(HtmlTag tag, HtmlAttribute attribute)
        {
            string tagNameInLowercase = tag.NameInLowercase;
            string attributeNameInLowercase = attribute.NameInLowercase;
            IList<HtmlAttribute> attributes = tag.Attributes;

            bool canMinify = false;

            if (tag.Flags.HasFlag(HtmlTagFlags.Custom))
            {
                string elementDirectiveName = AngularHelpers.NormalizeDirectiveName(tagNameInLowercase);

                switch (elementDirectiveName)
                {
                    case "ngPluralize":
                        canMinify = attributeNameInLowercase == "count" || attributeNameInLowercase == "when";
                        break;
                    case "ngMessages":
                        canMinify = attributeNameInLowercase == "for";
                        break;
                }
            }

            if (!canMinify)
            {
                string attributeDirectiveName = AngularHelpers.NormalizeDirectiveName(attributeNameInLowercase);
                canMinify = ContainsAngularBindingExpression(attributeDirectiveName);

                if (!canMinify)
                {
                    switch (attributeDirectiveName)
                    {
                        case "ngTrueValue":
                        case "ngFalseValue":
                            canMinify = tagNameInLowercase == "input" && attributes.Any(
                                a => a.NameInLowercase == "type" && a.Value.Trim().IgnoreCaseEquals("checkbox"));
                            break;
                    }
                }
            }

            return canMinify;
        }
        /// <summary>
        /// Checks whether remove an the attribute, that has empty value
        /// </summary>
        /// <param name="tag">Tag</param>
        /// <param name="attribute">Attribute</param>
        /// <returns>Result of check (true - can be removed; false - can not be removed)</returns>
        private static bool CanRemoveEmptyAttribute(HtmlTag tag, HtmlAttribute attribute)
        {
            string tagNameInLowercase = tag.NameInLowercase;
            string attributeNameInLowercase = attribute.NameInLowercase;
            string attributeValue = attribute.Value;
            HtmlAttributeType attributeType = attribute.Type;

            bool result = false;
            bool isZeroLengthString = attributeValue.Length == 0;

            if (isZeroLengthString || string.IsNullOrWhiteSpace(attributeValue))
            {
                if (tagNameInLowercase == "input" && attributeNameInLowercase == "value")
                {
                    result = isZeroLengthString;
                }
                else if (attributeType == HtmlAttributeType.Event
                    || (tagNameInLowercase == "form" && attributeNameInLowercase == "action")
                    || _emptyAttributesForRemoval.Contains(attributeNameInLowercase))
                {
                    result = true;
                }
            }

            return result;
        }
        /// <summary>
        /// Start tags handler
        /// </summary>
        /// <param name="context">Markup parsing context</param>
        /// <param name="tag">HTML tag</param>
        private void StartTagHandler(MarkupParsingContext context, HtmlTag tag)
        {
            HtmlNodeType previousNodeType = _currentNodeType;
            string previousTagName = string.Empty;
            if (_currentTag != null)
            {
                previousTagName = _currentTag.Name;
            }

            if (_settings.UseMetaCharsetTag && IsMetaContentTypeTag(tag.Name, tag.Attributes))
            {
                tag = UpgradeToMetaCharsetTag(tag);
            }

            _currentNodeType = HtmlNodeType.StartTag;
            _currentTag = tag;
            _currentText = string.Empty;

            string tagName = tag.Name;
            IList<HtmlAttribute> attributes = tag.Attributes;
            HtmlTagFlags tagFlags = tag.Flags;

            // Set whitespace flags for nested tags (for example <span> within a <pre>)
            WhitespaceMinificationMode whitespaceMinificationMode = _settings.WhitespaceMinificationMode;
            if (whitespaceMinificationMode != WhitespaceMinificationMode.None)
            {
                if (_tagsWithNotRemovableWhitespaceQueue.Count == 0)
                {
                    // Processing of whitespace, that followed before the start tag
                    bool allowTrimEnd = false;
                    if (tagFlags.Invisible || tagFlags.NonIndependent)
                    {
                        allowTrimEnd = true;
                    }
                    else
                    {
                        if (whitespaceMinificationMode == WhitespaceMinificationMode.Medium
                            || whitespaceMinificationMode == WhitespaceMinificationMode.Aggressive)
                        {
                            allowTrimEnd = tagFlags.Block;
                        }
                    }

                    if (allowTrimEnd)
                    {
                        TrimEndLastBufferItem();
                    }
                }

                if (!CanMinifyWhitespace(tagName))
                {
                    _tagsWithNotRemovableWhitespaceQueue.Enqueue(tagName);
                }
            }

            if (_settings.RemoveOptionalEndTags
                && previousNodeType != HtmlNodeType.StartTag
                && !IsSafeOptionalEndTag(previousTagName))
            {
                if (CanRemoveOptionalEndTagByNextTagName(previousTagName, tagName))
                {
                    RemoveLastEndTagFromBuffer(previousTagName);
                }

                FlushBuffer();
            }

            _buffer.Add("<");
            _buffer.Add(tagName);

            int attributeCount = attributes.Count;

            for (int attributeIndex = 0; attributeIndex < attributeCount; attributeIndex++)
            {
                _buffer.Add(BuildAttributeString(context, tag, attributes[attributeIndex]));
            }

            if (tagFlags.Empty)
            {
                if (_settings.EmptyTagRenderMode == HtmlEmptyTagRenderMode.Slash)
                {
                    _buffer.Add("/");
                }
                else if (_settings.EmptyTagRenderMode == HtmlEmptyTagRenderMode.SpaceAndSlash)
                {
                    _buffer.Add(" /");
                }
            }
            _buffer.Add(">");
        }
        /// <summary>
        /// Checks whether remove an the tag, that has empty content
        /// </summary>
        /// <param name="tag">Tag</param>
        /// <returns>Result of check (true - can be removed; false - can not be removed)</returns>
        private static bool CanRemoveTagWithoutContent(HtmlTag tag)
        {
            string tagNameInLowercase = tag.NameInLowercase;
            HtmlTagFlags tagFlags = tag.Flags;
            IList<HtmlAttribute> attributes = tag.Attributes;

            return !(tagFlags.HasFlag(HtmlTagFlags.Custom)
                || (tagFlags.HasFlag(HtmlTagFlags.Xml) && tagFlags.HasFlag(HtmlTagFlags.NonIndependent))
                || _unremovableEmptyTags.Contains(tagNameInLowercase)
                || attributes.Any(a => IsCustomAttribute(a)
                    || (_unremovableEmptyTagAttributes.Contains(a.NameInLowercase) && !string.IsNullOrWhiteSpace(a.Value))));
        }
        /// <summary>
        /// Minify HTML content
        /// </summary>
        /// <param name="content">HTML content</param>
        /// <param name="fileContext">File context</param>
        /// <param name="encoding">Text encoding</param>
        /// <param name="generateStatistics">Flag for whether to allow generate minification statistics</param>
        /// <returns>Minification result</returns>
        public MarkupMinificationResult Minify(string content, string fileContext, Encoding encoding,
			bool generateStatistics)
        {
            MinificationStatistics statistics = null;
            string minifiedContent = string.Empty;
            var errors = new List<MinificationErrorInfo>();
            var warnings = new List<MinificationErrorInfo>();

            lock (_minificationSynchronizer)
            {
                _fileContext = fileContext;
                _encoding = encoding;

                try
                {
                    if (generateStatistics)
                    {
                        statistics = new MinificationStatistics(_encoding);
                        statistics.Init(content);
                    }

                    _result = new StringBuilder(content.Length);
                    _buffer = new List<string>();
                    _errors = new List<MinificationErrorInfo>();
                    _warnings = new List<MinificationErrorInfo>();
                    _tagsWithNotRemovableWhitespaceQueue = new Queue<string>();
                    _currentNodeType = HtmlNodeType.Unknown;
                    _currentTag = null;
                    _currentText = string.Empty;

                    _htmlParser.Parse(content);

                    FlushBuffer();

                    if (_errors.Count == 0)
                    {
                        minifiedContent = _result.ToString();

                        if (generateStatistics)
                        {
                            statistics.End(minifiedContent);
                        }
                    }
                }
                catch (HtmlParsingException e)
                {
                    WriteError(LogCategoryConstants.HtmlParsingError, e.Message, _fileContext,
                        e.LineNumber, e.ColumnNumber, e.SourceFragment);
                }
                finally
                {
                    _result.Clear();
                    _buffer.Clear();
                    _tagsWithNotRemovableWhitespaceQueue.Clear();
                    _currentTag = null;

                    if (_errors.Count == 0)
                    {
                        _logger.Info(LogCategoryConstants.HtmlMinificationSuccess,
                            string.Format(Strings.SuccesMessage_MarkupMinificationComplete, "HTML"),
                            _fileContext, statistics);
                    }

                    errors.AddRange(_errors);
                    warnings.AddRange(_warnings);

                    _errors.Clear();
                    _warnings.Clear();
                    _fileContext = null;
                    _encoding = null;
                }
            }

            return new MarkupMinificationResult(minifiedContent, errors, warnings, statistics);
        }
        /// <summary>
        /// Checks whether remove whitespace after end non-independent tag
        /// </summary>
        /// <param name="endTag">End tag</param>
        /// <returns>Result of check (true - can be removed; false - can not be removed)</returns>
        private static bool CanRemoveWhitespaceAfterEndNonIndependentTag(HtmlTag endTag)
        {
            string endTagNameInLowercase = endTag.NameInLowercase;
            bool cannotRemove;

            switch (endTagNameInLowercase)
            {
                case "li":
                case "dt":
                case "dd":
                case "rt":
                case "rp":
                case "rb":
                case "rtc":
                    cannotRemove = true;
                    break;
                default:
                    cannotRemove = false;
                    break;
            }

            return !cannotRemove;
        }
        /// <summary>
        /// Cleans a attribute value
        /// </summary>
        /// <param name="context">Markup parsing context</param>
        /// <param name="tag">HTML tag</param>
        /// <param name="attribute">HTML attribute</param>
        /// <returns>Processed attribute value</returns>
        private string CleanAttributeValue(MarkupParsingContext context, HtmlTag tag, HtmlAttribute attribute)
        {
            string attributeValue = attribute.Value;
            if (attributeValue.Length == 0)
            {
                return attributeValue;
            }

            string result = attributeValue;
            string tagName = tag.Name;
            IList<HtmlAttribute> attributes = tag.Attributes;
            string attributeName = attribute.Name;
            HtmlAttributeType attributeType = attribute.Type;

            if (attributeType != HtmlAttributeType.Event && MustacheStyleTagHelpers.ContainsMustacheStyleTag(result))
            {
                // Processing of Angular Mustache-style tags
                var attributeValueBuilder = new StringBuilder();

                MustacheStyleTagHelpers.ParseMarkup(result,
                    (localContext, expression, startDelimiter, endDelimiter) =>
                    {
                        string processedExpression = expression;
                        if (_settings.MinifyAngularBindingExpressions && startDelimiter == "{{" && endDelimiter == "}}")
                        {
                            processedExpression = MinifyAngularBindingExpression(context, attribute.ValueCoordinates,
                                localContext.NodeCoordinates, expression);
                        }

                        attributeValueBuilder.Append(startDelimiter);
                        attributeValueBuilder.Append(processedExpression);
                        attributeValueBuilder.Append(endDelimiter);
                    },
                    (localContext, textValue) =>
                    {
                        string processedTextValue = textValue;
                        if (attributeType == HtmlAttributeType.ClassName)
                        {
                            processedTextValue = Utils.CollapseWhitespace(textValue);
                        }

                        attributeValueBuilder.Append(processedTextValue);
                    }
                );

                result = attributeValueBuilder.ToString();
                attributeValueBuilder.Clear();

                switch (attributeType)
                {
                    case HtmlAttributeType.Uri:
                    case HtmlAttributeType.Numeric:
                    case HtmlAttributeType.ClassName:
                        result = result.Trim();
                        break;
                    case HtmlAttributeType.Style:
                        result = result.Trim();
                        result = Utils.RemoveEndingSemicolon(result);
                        break;
                    default:
                        if (_settings.MinifyAngularBindingExpressions)
                        {
                            string elementDirectiveName = AngularHelpers.NormalizeDirectiveName(tagName);
                            if (elementDirectiveName == "ngPluralize" && attributeName == "when")
                            {
                                result = MinifyAngularBindingExpression(context, attribute.ValueCoordinates, result);
                            }
                        }

                        break;
                }
            }
            else
            {
                switch (attributeType)
                {
                    case HtmlAttributeType.Uri:
                        result = result.Trim();

                        if (result.StartsWith(HTTP_PROTOCOL, StringComparison.OrdinalIgnoreCase))
                        {
                            if (_settings.RemoveHttpProtocolFromAttributes && !ContainsRelExternalAttribute(attributes))
                            {
                                int httpProtocolLength = HTTP_PROTOCOL.Length;
                                result = result.Substring(httpProtocolLength);
                            }
                        }
                        else if (result.StartsWith(HTTPS_PROTOCOL, StringComparison.OrdinalIgnoreCase))
                        {
                            if (_settings.RemoveHttpsProtocolFromAttributes && !ContainsRelExternalAttribute(attributes))
                            {
                                int httpsProtocolLength = HTTPS_PROTOCOL.Length;
                                result = result.Substring(httpsProtocolLength);
                            }
                        }
                        else if (result == "href" && result.StartsWith(JS_PROTOCOL, StringComparison.OrdinalIgnoreCase))
                        {
                            result = ProcessInlineScriptContent(context, attribute);
                        }

                        break;
                    case HtmlAttributeType.Numeric:
                        result = result.Trim();
                        break;
                    case HtmlAttributeType.ClassName:
                        if (AngularHelpers.IsClassDirective(result))
                        {
                            // Processing of Angular class directives
                            string ngOriginalDirectiveName = string.Empty;
                            string ngNormalizedDirectiveName = string.Empty;
                            string ngExpression;
                            var ngDirectives = new Dictionary<string, string>();

                            AngularHelpers.ParseClassDirective(result,
                                (localContext, originalDirectiveName, normalizedDirectiveName) =>
                                {
                                    ngOriginalDirectiveName = originalDirectiveName;
                                    ngNormalizedDirectiveName = normalizedDirectiveName;
                                    ngExpression = null;

                                    ngDirectives.Add(ngOriginalDirectiveName, ngExpression);
                                },
                                (localContext, expression) =>
                                {
                                    ngExpression = expression;
                                    if (_settings.MinifyAngularBindingExpressions
                                        && ContainsAngularBindingExpression(ngNormalizedDirectiveName))
                                    {
                                        ngExpression = MinifyAngularBindingExpression(context,
                                            attribute.ValueCoordinates, localContext.NodeCoordinates,
                                            expression);
                                    }

                                    ngDirectives[ngOriginalDirectiveName] = ngExpression;
                                },
                                localContext =>
                                {
                                    if (ngDirectives[ngOriginalDirectiveName] == null)
                                    {
                                        ngDirectives[ngOriginalDirectiveName] = string.Empty;
                                    }
                                }
                            );

                            int directiveCount = ngDirectives.Count;
                            if (directiveCount > 0)
                            {
                                var directiveBuilder = new StringBuilder();
                                int directiveIndex = 0;
                                int lastDirectiveIndex = directiveCount - 1;
                                string previousExpression = null;

                                foreach (var directive in ngDirectives)
                                {
                                    string directiveName = directive.Key;
                                    string expression = directive.Value;

                                    if (directiveIndex > 0 && (expression == null || previousExpression == null))
                                    {
                                        directiveBuilder.Append(" ");
                                    }

                                    directiveBuilder.Append(directiveName);
                                    if (!string.IsNullOrWhiteSpace(expression))
                                    {
                                        directiveBuilder.AppendFormat(":{0}", expression);
                                    }

                                    if (directiveIndex < lastDirectiveIndex && expression != null)
                                    {
                                        directiveBuilder.Append(";");
                                    }

                                    previousExpression = expression;
                                    directiveIndex++;
                                }

                                result = directiveBuilder.ToString();
                                directiveBuilder.Clear();
                            }
                            else
                            {
                                result = string.Empty;
                            }
                        }
                        else
                        {
                            result = result.Trim();
                            result = Utils.CollapseWhitespace(result);
                        }

                        break;
                    case HtmlAttributeType.Style:
                        result = ProcessInlineStyleContent(context, attribute);
                        break;
                    case HtmlAttributeType.Event:
                        result = ProcessInlineScriptContent(context, attribute);
                        break;
                    default:
                        if (attributeName == "data-bind" && _settings.MinifyKnockoutBindingExpressions)
                        {
                            result = MinifyKnockoutBindingExpression(context, attribute);
                        }
                        else if (tagName == "meta" && attributeName == "content"
                            && attributes.Any(a => a.Name == "name" && a.Value.Trim().IgnoreCaseEquals("keywords")))
                        {
                            result = result.Trim();
                            result = Utils.CollapseWhitespace(result);
                            result = _separatingCommaWithSpacesRegex.Replace(result, ",");
                            result = _endingCommaWithSpacesRegex.Replace(result, string.Empty);
                        }
                        else
                        {
                            if (_settings.MinifyAngularBindingExpressions
                                && CanMinifyAngularBindingExpressionInAttribute(tagName, attributeName, attributes))
                            {
                                result = MinifyAngularBindingExpression(context, attribute.ValueCoordinates, result);
                            }
                        }

                        break;
                }
            }

            return result;
        }
        /// <summary>
        /// Checks whether remove whitespace after end non-independent tag by parent tag
        /// </summary>
        /// <param name="endTag">End tag</param>
        /// <param name="parentTag">Parent tag</param>
        /// <returns>Result of check (true - can be removed; false - can not be removed)</returns>
        private static bool CanRemoveWhitespaceAfterEndNonIndependentTagByParentTag(HtmlTag endTag, HtmlTag parentTag)
        {
            string endTagNameInLowercase = endTag.NameInLowercase;
            string parentTagNameInLowercase = parentTag.NameInLowercase;
            bool canRemove;

            switch (endTagNameInLowercase)
            {
                case "li":
                    canRemove = parentTagNameInLowercase == "ul" || parentTagNameInLowercase == "ol"
                        || parentTagNameInLowercase == "menu";
                    break;
                case "dt":
                case "dd":
                    canRemove = parentTagNameInLowercase == "dl";
                    break;
                default:
                    canRemove = false;
                    break;
            }

            return canRemove;
        }
Example #32
0
        /// <summary>
        /// Parses a start tag
        /// </summary>
        /// <param name="tagName">Tag name</param>
        /// <param name="tagNameInLowercase">Tag name in lowercase</param>
        /// <param name="attributesString">String representation of the attribute list</param>
        /// <param name="attributesCoordinates">Attributes coordinates</param>
        /// <param name="isEmptyTag">Flag that tag is empty</param>
        private void ParseStartTag(string tagName, string tagNameInLowercase, string attributesString,
			SourceCodeNodeCoordinates attributesCoordinates, bool isEmptyTag)
        {
            HtmlTagFlags tagFlags = GetTagFlagsByName(tagNameInLowercase);

            if (tagFlags.HasFlag(HtmlTagFlags.Optional))
            {
                HtmlTag lastStackedTag = _tagStack.LastOrDefault();
                if (lastStackedTag != null && lastStackedTag.NameInLowercase == tagNameInLowercase)
                {
                    ParseEndTag(lastStackedTag.Name, lastStackedTag.NameInLowercase);
                }
                else
                {
                    if (tagNameInLowercase == "body" && _tagStack.Any(t => t.NameInLowercase == "head"))
                    {
                        HtmlTag headTag = _tagStack.Single(t => t.NameInLowercase == "head");
                        ParseEndTag(headTag.Name, headTag.NameInLowercase);
                    }
                }
            }

            if (tagFlags.HasFlag(HtmlTagFlags.Empty))
            {
                isEmptyTag = true;
            }
            else if (isEmptyTag)
            {
                tagFlags |= HtmlTagFlags.Empty;
            }

            var attributes = ParseAttributes(tagName, tagNameInLowercase, tagFlags,
                attributesString, attributesCoordinates);
            var tag = new HtmlTag(tagName, tagNameInLowercase, attributes, tagFlags);

            if (!isEmptyTag)
            {
                if (_conditionalCommentOpened)
                {
                    HtmlConditionalComment lastConditionalComment = _conditionalCommentStack.Peek();
                    HtmlConditionalCommentType lastConditionalCommentType = lastConditionalComment.Type;

                    if (tagFlags.HasFlag(HtmlTagFlags.EmbeddedCode)
                        || lastConditionalCommentType == HtmlConditionalCommentType.RevealedValidating
                        || lastConditionalCommentType == HtmlConditionalCommentType.RevealedValidatingSimplified)
                    {
                        _tagStack.Add(tag);
                    }
                }
                else
                {
                    _tagStack.Add(tag);
                }
            }

            if (_handlers.StartTag != null)
            {
                _handlers.StartTag(_context, tag);
            }

            if (tagFlags.HasFlag(HtmlTagFlags.Xml) && !tagFlags.HasFlag(HtmlTagFlags.NonIndependent))
            {
                _xmlTagStack.Push(tagNameInLowercase);
            }
        }
Example #33
0
        /// <summary>
        /// Parses HTML content
        /// </summary>
        /// <param name="content">HTML content</param>
        public void Parse(string content)
        {
            int contentLength = content.Length;

            if (contentLength == 0)
            {
                return;
            }

            lock (_parsingSynchronizer)
            {
                _innerContext = new InnerMarkupParsingContext(content);
                _context      = new MarkupParsingContext(_innerContext);

                int endPosition      = contentLength - 1;
                int previousPosition = -1;

                try
                {
                    while (_innerContext.Position <= endPosition)
                    {
                        bool    isProcessed    = false;
                        HtmlTag lastStackedTag = _tagStack.LastOrDefault();

                        // Make sure we're not in a tag, that contains embedded code
                        if (lastStackedTag == null || !lastStackedTag.Flags.IsSet(HtmlTagFlags.EmbeddedCode))
                        {
                            if (_innerContext.PeekCurrentChar() == '<')
                            {
                                switch (_innerContext.PeekNextChar())
                                {
                                case char c when c.IsAlphaNumeric():
                                    // Start tag
                                    isProcessed = ProcessStartTag();

                                    break;

                                case '/':
                                    if (_innerContext.PeekNextChar().IsAlphaNumeric())
                                    {
                                        // End tag
                                        isProcessed = ProcessEndTag();
                                    }
                                    break;

                                case '!':
                                    switch (_innerContext.PeekNextChar())
                                    {
                                    case '-':
                                        if (_innerContext.PeekNextChar() == '-')
                                        {
                                            // Comments
                                            if (_innerContext.PeekNextChar() == '[')
                                            {
                                                // Revealed validating If conditional comments
                                                // (e.g. <!--[if ... ]><!--> or <!--[if ... ]>-->)
                                                isProcessed = ProcessRevealedValidatingIfComment();

                                                if (!isProcessed)
                                                {
                                                    // Hidden If conditional comments (e.g. <!--[if ... ]>)
                                                    isProcessed = ProcessHiddenIfComment();
                                                }
                                            }
                                            else
                                            {
                                                // Revealed validating End If conditional comments
                                                // (e.g. <!--<![endif]-->)
                                                isProcessed = ProcessRevealedValidatingEndIfComment();
                                            }

                                            if (!isProcessed)
                                            {
                                                // HTML comments
                                                isProcessed = ProcessComment();
                                            }
                                        }
                                        break;

                                    case '[':
                                        switch (_innerContext.PeekNextChar())
                                        {
                                        case 'i':
                                        case 'I':
                                            // Revealed If conditional comment (e.g. <![if ... ]>)
                                            isProcessed = ProcessRevealedIfComment();
                                            break;

                                        case 'e':
                                        case 'E':
                                            // Hidden End If conditional comment (e.g. <![endif]-->)
                                            isProcessed = ProcessHiddenEndIfComment();

                                            if (!isProcessed)
                                            {
                                                // Revealed End If conditional comment (e.g. <![endif]>)
                                                isProcessed = ProcessRevealedEndIfComment();
                                            }
                                            break;

                                        case 'C':
                                            // CDATA sections
                                            isProcessed = ProcessCdataSection();
                                            break;
                                        }
                                        break;

                                    case 'D':
                                    case 'd':
                                        // Doctype declaration
                                        isProcessed = ProcessDoctype();
                                        break;
                                    }
                                    break;

                                case '?':
                                    // XML declaration
                                    isProcessed = ProcessXmlDeclaration();
                                    break;
                                }
                            }

                            if (!isProcessed)
                            {
                                // Text
                                ProcessText();
                            }
                        }
                        else
                        {
                            // Embedded code
                            ProcessEmbeddedCode();
                        }

                        if (_innerContext.Position == previousPosition)
                        {
                            throw new MarkupParsingException(
                                      string.Format(Strings.ErrorMessage_MarkupParsingFailed, "HTML"),
                                      _innerContext.NodeCoordinates, _innerContext.GetSourceFragment());
                        }

                        previousPosition = _innerContext.Position;
                    }

                    // Clean up any remaining tags
                    ParseEndTag();

                    // Check whether there were not closed conditional comment
                    if (_conditionalCommentStack.Count > 0)
                    {
                        throw new MarkupParsingException(
                                  Strings.ErrorMessage_NotClosedConditionalComment,
                                  _innerContext.NodeCoordinates, _innerContext.GetSourceFragment());
                    }
                }
                catch (MarkupParsingException)
                {
                    throw;
                }
                finally
                {
                    _tagStack.Clear();
                    _tempAttributes.Clear();
                    _conditionalCommentStack.Clear();
                    _conditionalCommentOpened = false;
                    _xmlTagStack.Clear();
                    _context      = null;
                    _innerContext = null;
                }
            }
        }