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