/// <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> /// Minify a Knockout binding expression /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="attribute">Attribute</param> /// <returns>Minified binding expression</returns> private string MinifyKnockoutBindingExpression(MarkupParsingContext context, HtmlAttribute attribute) { return MinifyKnockoutBindingExpression(context, attribute.ValueCoordinates, SourceCodeNodeCoordinates.Empty, attribute.Value); }
/// <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> /// 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> /// 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> /// Checks whether the attribute is custom /// </summary> /// <param name="attribute">Attribute</param> /// <returns>Result of check</returns> private static bool IsCustomAttribute(HtmlAttribute attribute) { bool isCustomAttribute = false; if (attribute.Type == HtmlAttributeType.Text) { string attributeNameInLowercase = attribute.NameInLowercase; int charCount = attributeNameInLowercase.Length; for (int charIndex = 0; charIndex < charCount; charIndex++) { char charValue = attributeNameInLowercase[charIndex]; if (!charValue.IsAlphaLower()) { isCustomAttribute = true; break; } } if (isCustomAttribute) { isCustomAttribute = attributeNameInLowercase != "accept-charset" && attributeNameInLowercase != "http-equiv"; } } return isCustomAttribute; }
/// <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> /// 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> /// Checks whether it is possible to remove the attribute quotes /// </summary> /// <param name="attribute">Attribute</param> /// <param name="attributeQuotesRemovalMode">Removal mode of HTML attribute quotes</param> /// <returns>Result of check (true - can remove; false - cannot remove)</returns> private static bool CanRemoveAttributeQuotes(HtmlAttribute attribute, HtmlAttributeQuotesRemovalMode attributeQuotesRemovalMode) { string attributeValue = attribute.Value; bool result = false; if (attributeQuotesRemovalMode != HtmlAttributeQuotesRemovalMode.KeepQuotes) { if (!attributeValue.EndsWith("/")) { if (attributeQuotesRemovalMode == HtmlAttributeQuotesRemovalMode.Html4) { result = _html4AttributeValueNotRequireQuotesRegex.IsMatch(attributeValue); } else if (attributeQuotesRemovalMode == HtmlAttributeQuotesRemovalMode.Html5) { result = CommonRegExps.Html5AttributeValueNotRequireQuotes.IsMatch(attributeValue); } } } return result; }
/// <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; }
private HtmlAttributeViewModel InnerBuildAttributeViewModel(HtmlAttribute attribute, bool omitValue, bool addQuotes) { string displayAttributeName = CanPreserveCase() ? attribute.Name : attribute.NameInLowercase; string encodedAttributeValue = !omitValue ? HtmlAttribute.HtmlAttributeEncode(attribute.Value, HtmlAttributeQuotesType.Double) : null; var attributeViewModel = new HtmlAttributeViewModel(displayAttributeName, encodedAttributeValue, addQuotes); return attributeViewModel; }
/// <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 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 bool CanRemoveAttribute(HtmlTag tag, HtmlAttribute attribute) { if (_settings.PreservableAttributeCollection.Count == 0) { return true; } string tagNameInLowercase = tag.NameInLowercase; string attributeNameInLowercase = attribute.NameInLowercase; string attributeValue = attribute.Value; bool result = true; foreach (HtmlAttributeExpression attributeExpression in _settings.PreservableAttributeCollection) { bool cannotRemove = attributeExpression.IsMatch(tagNameInLowercase, attributeNameInLowercase, attributeValue); if (cannotRemove) { result = false; break; } } return result; }
/// <summary> /// Processes a inline script content /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="attribute">Attribute</param> /// <returns>Processed inline script content</returns> private string ProcessInlineScriptContent(MarkupParsingContext context, HtmlAttribute attribute) { string scriptContent = attribute.Value; bool forHrefAttribute = (attribute.Name == "href"); string result = scriptContent; if (_settings.MinifyInlineJsCode && _jsMinifier.IsInlineCodeMinificationSupported) { bool isJavascriptProtocolRemoved = false; if (scriptContent.StartsWith(JS_PROTOCOL, StringComparison.OrdinalIgnoreCase)) { result = _jsProtocolRegex.Replace(result, string.Empty); isJavascriptProtocolRemoved = true; } CodeMinificationResult minificationResult = _jsMinifier.Minify(result, true); if (minificationResult.Errors.Count == 0) { result = minificationResult.MinifiedContent ?? string.Empty; } if (minificationResult.Errors.Count > 0 || minificationResult.Warnings.Count > 0) { string sourceCode = context.SourceCode; SourceCodeNodeCoordinates tagCoordinates = context.NodeCoordinates; SourceCodeNodeCoordinates attributeCoordinates = attribute.ValueCoordinates; foreach (MinificationErrorInfo error in minificationResult.Errors) { var relativeErrorCoordinates = new SourceCodeNodeCoordinates(error.LineNumber, error.ColumnNumber); SourceCodeNodeCoordinates absoluteErrorCoordinates = CalculateAbsoluteInlineCodeErrorCoordinates( tagCoordinates, attributeCoordinates, relativeErrorCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( sourceCode, absoluteErrorCoordinates); string message = error.Message.Trim(); WriteError(LogCategoryConstants.JsMinificationError, message, _fileContext, absoluteErrorCoordinates.LineNumber, absoluteErrorCoordinates.ColumnNumber, sourceFragment); } foreach (MinificationErrorInfo warning in minificationResult.Warnings) { var relativeErrorCoordinates = new SourceCodeNodeCoordinates(warning.LineNumber, warning.ColumnNumber); SourceCodeNodeCoordinates absoluteErrorCoordinates = CalculateAbsoluteInlineCodeErrorCoordinates( tagCoordinates, attributeCoordinates, relativeErrorCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( sourceCode, absoluteErrorCoordinates); string message = warning.Message.Trim(); WriteWarning(LogCategoryConstants.JsMinificationWarning, message, _fileContext, absoluteErrorCoordinates.LineNumber, absoluteErrorCoordinates.ColumnNumber, sourceFragment); } } if (isJavascriptProtocolRemoved && (forHrefAttribute || !_settings.RemoveJsProtocolFromAttributes)) { result = JS_PROTOCOL + result; } } else { result = result.Trim(); if (!forHrefAttribute && _settings.RemoveJsProtocolFromAttributes) { result = _jsProtocolRegex.Replace(result, string.Empty); } } result = Utils.RemoveEndingSemicolon(result); return result; }
/// <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> /// Processes a inline style content /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="attribute">Attribute</param> /// <returns>Processed inline style content</returns> private string ProcessInlineStyleContent(MarkupParsingContext context, HtmlAttribute attribute) { string styleContent = attribute.Value; string result = styleContent; if (_settings.MinifyInlineCssCode && _cssMinifier.IsInlineCodeMinificationSupported) { CodeMinificationResult minificationResult = _cssMinifier.Minify(result, true); if (minificationResult.Errors.Count == 0) { result = minificationResult.MinifiedContent ?? string.Empty; } if (minificationResult.Errors.Count > 0 || minificationResult.Warnings.Count > 0) { string sourceCode = context.SourceCode; SourceCodeNodeCoordinates tagCoordinates = context.NodeCoordinates; SourceCodeNodeCoordinates attributeCoordinates = attribute.ValueCoordinates; foreach (MinificationErrorInfo error in minificationResult.Errors) { var relativeErrorCoordinates = new SourceCodeNodeCoordinates(error.LineNumber, error.ColumnNumber); SourceCodeNodeCoordinates absoluteErrorCoordinates = CalculateAbsoluteInlineCodeErrorCoordinates( tagCoordinates, attributeCoordinates, relativeErrorCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( sourceCode, absoluteErrorCoordinates); string message = error.Message.Trim(); WriteError(LogCategoryConstants.CssMinificationError, message, _fileContext, absoluteErrorCoordinates.LineNumber, absoluteErrorCoordinates.ColumnNumber, sourceFragment); } foreach (MinificationErrorInfo warning in minificationResult.Warnings) { var relativeErrorCoordinates = new SourceCodeNodeCoordinates(warning.LineNumber, warning.ColumnNumber); SourceCodeNodeCoordinates absoluteErrorCoordinates = CalculateAbsoluteInlineCodeErrorCoordinates( tagCoordinates, attributeCoordinates, relativeErrorCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( sourceCode, absoluteErrorCoordinates); string message = warning.Message.Trim(); WriteWarning(LogCategoryConstants.CssMinificationWarning, message, _fileContext, absoluteErrorCoordinates.LineNumber, absoluteErrorCoordinates.ColumnNumber, sourceFragment); } } } else { result = result.Trim(); } result = Utils.RemoveEndingSemicolon(result); return result; }
/// <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> /// 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> /// Process a attributes /// </summary> /// <returns>List of attributes</returns> private List <HtmlAttribute> ProcessAttributes() { string content = _innerContext.SourceCode; int currentPosition = _innerContext.Position; SourceCodeNodeCoordinates currentCoordinates = _innerContext.NodeCoordinates; Match match = _attributeRegex.Match(content, currentPosition, _innerContext.RemainderLength); while (match.Success) { GroupCollection groups = match.Groups; Group attributeNameGroup = groups["attributeName"]; Group attributeEqualSignGroup = groups["attributeEqualSign"]; Group attributeValueGroup = groups["attributeValue"]; string attributeName = attributeNameGroup.Value; string attributeNameInLowercase = attributeName; if (Utils.ContainsUppercaseCharacters(attributeName)) { attributeNameInLowercase = attributeName.ToLowerInvariant(); } string attributeValue = null; if (attributeEqualSignGroup.Success) { if (attributeValueGroup.Success) { attributeValue = attributeValueGroup.Value; if (!string.IsNullOrWhiteSpace(attributeValue)) { attributeValue = HtmlAttributeValueHelpers.Decode(attributeValue); } } else { attributeValue = string.Empty; } } var attributeNameCoordinates = SourceCodeNodeCoordinates.Empty; int attributeNamePosition = -1; if (attributeNameGroup.Success) { attributeNamePosition = attributeNameGroup.Index; } if (attributeNamePosition != -1) { int lineBreakCount; int charRemainderCount; SourceCodeNavigator.CalculateLineBreakCount(content, currentPosition, attributeNamePosition - currentPosition, out lineBreakCount, out charRemainderCount); attributeNameCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( currentCoordinates, lineBreakCount, charRemainderCount); currentPosition = attributeNamePosition; currentCoordinates = attributeNameCoordinates; } var attributeValueCoordinates = SourceCodeNodeCoordinates.Empty; int attributeValuePosition = -1; if (attributeValueGroup.Success) { attributeValuePosition = attributeValueGroup.Index; } if (attributeValuePosition != -1) { int lineBreakCount; int charRemainderCount; SourceCodeNavigator.CalculateLineBreakCount(content, currentPosition, attributeValuePosition - currentPosition, out lineBreakCount, out charRemainderCount); attributeValueCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( currentCoordinates, lineBreakCount, charRemainderCount); currentPosition = attributeValuePosition; currentCoordinates = attributeValueCoordinates; } var attribute = new HtmlAttribute(attributeName, attributeNameInLowercase, attributeValue, HtmlAttributeType.Unknown, attributeNameCoordinates, attributeValueCoordinates); _tempAttributes.Add(attribute); _innerContext.IncreasePosition(match.Length); match = _attributeRegex.Match(content, _innerContext.Position, _innerContext.RemainderLength); } int attributeCount = _tempAttributes.Count; var attributes = new List <HtmlAttribute>(attributeCount); for (int attributeIndex = 0; attributeIndex < attributeCount; attributeIndex++) { attributes.Add(_tempAttributes[attributeIndex]); } _tempAttributes.Clear(); return(attributes); }