/// <summary> /// Parses a Knockout begin containerless comment /// </summary> /// <param name="commentText">Comment text</param> /// <param name="expressionHandler">Binding expression handler</param> public static void ParseBeginContainerlessComment(string commentText, ExpressionDelegate expressionHandler) { Match koBeginContainerlessCommentMatch = _koBeginContainerlessCommentRegex.Match(commentText); if (koBeginContainerlessCommentMatch.Success) { var innerContext = new InnerMarkupParsingContext(commentText); var context = new MarkupParsingContext(innerContext); Group expressionGroup = koBeginContainerlessCommentMatch.Groups["expression"]; int expressionPosition = expressionGroup.Index; string expression = expressionGroup.Value.TrimEnd(); innerContext.IncreasePosition(expressionPosition); if (expressionHandler != null) { expressionHandler(context, expression); } } }
/// <summary> /// Processing instruction handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="instructionName">Instruction name</param> /// <param name="attributes">List of attributes</param> private void ProcessingInstructionHandler(MarkupParsingContext context, string instructionName, IList<XmlAttribute> attributes) { _currentNodeType = XmlNodeType.ProcessingInstruction; if (_settings.MinifyWhitespace) { RemoveLastWhitespaceBufferItems(); } _buffer.Add("<?"); _buffer.Add(instructionName); RenderAttributes(attributes); _buffer.Add("?>"); }
/// <summary> /// Empty tags handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="tagName">Tag name</param> /// <param name="attributes">List of attributes</param> private void EmptyTagHandler(MarkupParsingContext context, string tagName, IList<XmlAttribute> attributes) { XmlNodeType previousNodeType = _currentNodeType; _currentNodeType = XmlNodeType.EmptyTag; _currentText = string.Empty; if (_settings.MinifyWhitespace && previousNodeType == XmlNodeType.Text && (_startTagBeforeText || _endTagBeforeText || _emptyTagBeforeText)) { RemoveLastWhitespaceBufferItems(); } _buffer.Add("<"); _buffer.Add(tagName); RenderAttributes(attributes); _buffer.Add(_settings.RenderEmptyTagsWithSpace ? " />" : "/>"); }
/// <summary> /// Comments handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="commentText">Comment text</param> private void CommentHandler(MarkupParsingContext context, string commentText) { if (!_settings.RemoveXmlComments) { _currentNodeType = XmlNodeType.Comment; _buffer.Add("<!--"); _buffer.Add(commentText); _buffer.Add("-->"); } }
/// <summary> /// Parses XML content /// </summary> /// <param name="content">XML 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; int firstCharPosition = _innerContext.Position; char firstCharValue; bool firstCharExist = content.TryGetChar(firstCharPosition, out firstCharValue); if (firstCharExist && firstCharValue == '<') { int secondCharPosition = firstCharPosition + 1; char secondCharValue; bool secondCharExist = content.TryGetChar(secondCharPosition, out secondCharValue); if (secondCharExist) { if (IsTagFirstChar(secondCharValue)) { // Start tag isProcessed = ProcessStartTag(); } else { int thirdCharPosition = secondCharPosition + 1; char thirdCharValue; bool thirdCharExist = content.TryGetChar(thirdCharPosition, out thirdCharValue); if (thirdCharExist) { switch (secondCharValue) { case '/': if (IsTagFirstChar(thirdCharValue)) { // End tag isProcessed = ProcessEndTag(); } break; case '!': switch (thirdCharValue) { case '-': int fourthCharPosition = thirdCharPosition + 1; char fourthCharValue; bool fourthCharExist = content.TryGetChar( fourthCharPosition, out fourthCharValue); if (fourthCharExist && fourthCharValue == '-') { // XML comments isProcessed = ProcessComment(); } break; case '[': // CDATA sections isProcessed = ProcessCdataSection(); break; case 'D': case 'd': // Doctype declaration isProcessed = ProcessDoctype(); break; } break; case '?': // XML declaration and processing instructions isProcessed = ProcessProcessingInstruction(); break; } } } } } if (!isProcessed) { // Text ProcessText(); } if (_innerContext.Position == previousPosition) { throw new MarkupParsingException( string.Format(Strings.ErrorMessage_MarkupParsingFailed, "XML"), _innerContext.NodeCoordinates, _innerContext.GetSourceFragment()); } previousPosition = _innerContext.Position; } // Check whether there were not closed tags if (_tagStack.Count > 0) { StackedXmlTag stackedTag = _tagStack.Pop(); throw new MarkupParsingException( string.Format(Strings.ErrorMessage_NotClosedTag, stackedTag.Name), stackedTag.Coordinates, SourceCodeNavigator.GetSourceFragment(_innerContext.SourceCode, stackedTag.Coordinates)); } } catch (MarkupParsingException) { throw; } finally { _tagStack.Clear(); _context = null; _innerContext = null; } } }
/// <summary> /// Parses a Angular class directive /// </summary> /// <param name="className">Class name</param> /// <param name="directiveNameHandler">Directive name handler</param> /// <param name="expressionHandler">Binding expression handler</param> /// <param name="semicolonHandler">Semicolon handler</param> public static void ParseClassDirective(string className, DirectiveNameDelegate directiveNameHandler, ExpressionDelegate expressionHandler, SemicolonDelegate semicolonHandler) { MatchCollection ngClassDirectiveMatches = _ngClassDirectiveRegex.Matches(className); if (ngClassDirectiveMatches.Count > 0) { var innerContext = new InnerMarkupParsingContext(className); var context = new MarkupParsingContext(innerContext); int currentPosition = 0; foreach (Match ngClassDirectiveMatch in ngClassDirectiveMatches) { GroupCollection groups = ngClassDirectiveMatch.Groups; Group directiveNameGroup = groups["directiveName"]; int directiveNamePosition = directiveNameGroup.Index; string originalDirectiveName = directiveNameGroup.Value; string normalizedDirectiveName = NormalizeDirectiveName(originalDirectiveName); innerContext.IncreasePosition(directiveNamePosition - currentPosition); currentPosition = directiveNamePosition; if (directiveNameHandler != null) { directiveNameHandler(context, originalDirectiveName, normalizedDirectiveName); } Group expressionGroup = groups["expression"]; if (expressionGroup.Success) { int expressionPosition = expressionGroup.Index; string expression = expressionGroup.Value.Trim(); innerContext.IncreasePosition(expressionPosition - currentPosition); currentPosition = expressionPosition; if (expressionHandler != null) { expressionHandler(context, expression); } } Group semicolonGroup = groups["semicolon"]; if (semicolonGroup.Success) { int semicolonPosition = semicolonGroup.Index; innerContext.IncreasePosition(semicolonPosition - currentPosition); currentPosition = semicolonPosition; if (semicolonHandler != null) { semicolonHandler(context); } } } } }
/// <summary> /// Parses XML content /// </summary> /// <param name="content">XML 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; if (_innerContext.PeekCurrentChar() == '<') { switch (_innerContext.PeekNextChar()) { case char c when IsTagFirstChar(c): // Start tag isProcessed = ProcessStartTag(); break; case '/': if (IsTagFirstChar(_innerContext.PeekNextChar())) { // End tag isProcessed = ProcessEndTag(); } break; case '!': switch (_innerContext.PeekNextChar()) { case '-': if (_innerContext.PeekNextChar() == '-') { // XML comments isProcessed = ProcessComment(); } break; case '[': // CDATA sections isProcessed = ProcessCdataSection(); break; case 'D': // Doctype declaration isProcessed = ProcessDoctype(); break; } break; case '?': // XML declaration and processing instructions isProcessed = ProcessProcessingInstruction(); break; } } if (!isProcessed) { // Text ProcessText(); } if (_innerContext.Position == previousPosition) { throw new MarkupParsingException( string.Format(Strings.ErrorMessage_MarkupParsingFailed, "XML"), _innerContext.NodeCoordinates, _innerContext.GetSourceFragment()); } previousPosition = _innerContext.Position; } // Check whether there were not closed tags if (_tagStack.Count > 0) { StackedXmlTag stackedTag = _tagStack.Pop(); throw new MarkupParsingException( string.Format(Strings.ErrorMessage_NotClosedTag, stackedTag.Name), stackedTag.Coordinates, SourceCodeNavigator.GetSourceFragment(_innerContext.SourceCode, stackedTag.Coordinates)); } } catch (MarkupParsingException) { throw; } finally { _tagStack.Clear(); _context = null; _innerContext = null; } } }
/// <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.HasFlag(HtmlTagFlags.EmbeddedCode)) { int firstCharPosition = _innerContext.Position; char firstCharValue; bool firstCharExist = content.TryGetChar(firstCharPosition, out firstCharValue); if (firstCharExist && firstCharValue == '<') { int secondCharPosition = firstCharPosition + 1; char secondCharValue; bool secondCharExist = content.TryGetChar(secondCharPosition, out secondCharValue); if (secondCharExist) { if (secondCharValue.IsAlphaNumeric()) { // Start tag isProcessed = ProcessStartTag(); } else { int thirdCharPosition = secondCharPosition + 1; char thirdCharValue; bool thirdCharExist = content.TryGetChar(thirdCharPosition, out thirdCharValue); if (thirdCharExist) { switch (secondCharValue) { case '/': if (thirdCharValue.IsAlphaNumeric()) { isProcessed = ProcessEndTag(); } break; case '!': switch (thirdCharValue) { case '-': int fourthCharPosition = thirdCharPosition + 1; char fourthCharValue; bool fourthCharExist = content.TryGetChar(fourthCharPosition, out fourthCharValue); if (fourthCharExist && fourthCharValue == '-') { // Comments int fifthCharPosition = fourthCharPosition + 1; char fifthCharValue; bool fifthCharExist = content.TryGetChar(fifthCharPosition, out fifthCharValue); if (fifthCharExist) { if (fifthCharValue == '[') { // 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 '[': // Remaining conditional comments // Hidden End If conditional comment (e.g. <![endif]-->) isProcessed = ProcessHiddenEndIfComment(); if (!isProcessed) { // Revealed If conditional comment (e.g. <![if ... ]>) isProcessed = ProcessRevealedIfComment(); } if (!isProcessed) { // Revealed End If conditional comment (e.g. <![endif]>) isProcessed = ProcessRevealedEndIfComment(); } 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(); _htmlTagFlagsCache.Clear(); _customHtmlTagFlagsCache.Clear(); _conditionalCommentStack.Clear(); _conditionalCommentOpened = false; _xmlTagStack.Clear(); _context = null; _innerContext = null; } } }
/// <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> /// Processes a embedded SVG content /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="svgContent">Embedded SVG content</param> /// <returns>Processed embedded SVG content</returns> private string ProcessEmbeddedSvgContent(MarkupParsingContext context, string svgContent) { string result = string.Empty; XmlMinifier innerXmlMinifier = GetInnerXmlMinifierInstance(); MarkupMinificationResult minificationResult = innerXmlMinifier.Minify(svgContent); if (minificationResult.Errors.Count == 0) { result = minificationResult.MinifiedContent ?? string.Empty; } else { string sourceCode = context.SourceCode; var documentCoordinates = context.NodeCoordinates; foreach (MinificationErrorInfo error in minificationResult.Errors) { var xmlNodeCoordinates = new SourceCodeNodeCoordinates(error.LineNumber, error.ColumnNumber); var absoluteNodeCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( documentCoordinates, xmlNodeCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( sourceCode, absoluteNodeCoordinates); string message = string.Format(Strings.ErrorMessage_MarkupMinificationFailed, "SVG", error.Message); WriteError(LogCategoryConstants.HtmlMinificationError, message, _fileContext, absoluteNodeCoordinates.LineNumber, absoluteNodeCoordinates.ColumnNumber, sourceFragment); } } return result; }
/// <summary> /// Processes a embedded style content /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="content">Embedded style content</param> /// <param name="contentType">Content type (MIME type) of the style</param> /// <returns>Processed embedded style content</returns> private string ProcessEmbeddedStyleContent(MarkupParsingContext context, string content, string contentType) { string processedContentType = (!string.IsNullOrWhiteSpace(contentType)) ? contentType.Trim().ToLowerInvariant() : CSS_CONTENT_TYPE; bool minifyWhitespace = (_settings.WhitespaceMinificationMode != WhitespaceMinificationMode.None); bool removeHtmlComments = _settings.RemoveHtmlCommentsFromScriptsAndStyles; bool removeCdataSections = _settings.RemoveCdataSectionsFromScriptsAndStyles; string startPart = string.Empty; string endPart = string.Empty; string code; string beforeCodeContent = string.Empty; if (_styleBeginCdataSectionRegex.IsMatch(content)) { beforeCodeContent = _styleBeginCdataSectionRegex.Match(content).Value; if (!removeCdataSections) { startPart = "/*<![CDATA[*/"; endPart = "/*]]>*/"; } code = _styleBeginCdataSectionRegex.Replace(content, string.Empty); code = _styleEndCdataSectionRegex.Replace(code, string.Empty); } else if (_styleBeginMaxCompatibleCdataSectionRegex.IsMatch(content)) { beforeCodeContent = _styleBeginMaxCompatibleCdataSectionRegex.Match(content).Value; if (!removeCdataSections) { startPart = "<!--/*--><![CDATA[/*><!--*/"; endPart = "/*]]>*/-->"; } code = _styleBeginMaxCompatibleCdataSectionRegex.Replace(content, string.Empty); code = _styleEndMaxCompatibleCdataSectionRegex.Replace(code, string.Empty); } else if (_styleBeginHtmlCommentRegex.IsMatch(content)) { beforeCodeContent = _styleBeginHtmlCommentRegex.Match(content).Value; if (!removeHtmlComments) { startPart = "<!--"; endPart = "-->"; } code = _styleBeginHtmlCommentRegex.Replace(content, string.Empty); code = _styleEndHtmlCommentRegex.Replace(code, string.Empty); } else { code = content; } if (processedContentType == CSS_CONTENT_TYPE && _settings.MinifyEmbeddedCssCode) { CodeMinificationResult minificationResult = _cssMinifier.Minify(code, false); if (minificationResult.Errors.Count == 0) { code = minificationResult.MinifiedContent ?? string.Empty; } if (minificationResult.Errors.Count > 0 || minificationResult.Warnings.Count > 0) { string sourceCode = context.SourceCode; var documentNodeCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( context.NodeCoordinates, beforeCodeContent); foreach (MinificationErrorInfo error in minificationResult.Errors) { var embeddedContentNodeCoordinates = new SourceCodeNodeCoordinates(error.LineNumber, error.ColumnNumber); var absoluteNodeCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( documentNodeCoordinates, embeddedContentNodeCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( sourceCode, absoluteNodeCoordinates); string message = error.Message.Trim(); WriteError(LogCategoryConstants.CssMinificationError, message, _fileContext, absoluteNodeCoordinates.LineNumber, absoluteNodeCoordinates.ColumnNumber, sourceFragment); } foreach (MinificationErrorInfo warning in minificationResult.Warnings) { var embeddedContentNodeCoordinates = new SourceCodeNodeCoordinates(warning.LineNumber, warning.ColumnNumber); var absoluteNodeCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( documentNodeCoordinates, embeddedContentNodeCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( sourceCode, absoluteNodeCoordinates); string message = warning.Message.Trim(); WriteWarning(LogCategoryConstants.CssMinificationWarning, message, _fileContext, absoluteNodeCoordinates.LineNumber, absoluteNodeCoordinates.ColumnNumber, sourceFragment); } } } if (minifyWhitespace && code.Length > 0) { code = code.Trim(); } string processedStyleContent; if (startPart.Length > 0 || endPart.Length > 0) { processedStyleContent = string.Concat(startPart, code, endPart); } else { processedStyleContent = code; } return processedStyleContent; }
/// <summary> /// Processes a embedded script content /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="content">Embedded script content</param> /// <param name="contentType">Content type (MIME type) of the script</param> /// <returns>Processed embedded script content</returns> private string ProcessEmbeddedScriptContent(MarkupParsingContext context, string content, string contentType) { string processedScriptContent = content; string processedContentType = (!string.IsNullOrWhiteSpace(contentType)) ? contentType.Trim().ToLowerInvariant() : JS_CONTENT_TYPE; bool isJavaScript = _jsContentTypes.Contains(processedContentType); bool isVbScript = (processedContentType == VBS_CONTENT_TYPE); bool minifyWhitespace = (_settings.WhitespaceMinificationMode != WhitespaceMinificationMode.None); if (isJavaScript || isVbScript) { bool removeHtmlComments = _settings.RemoveHtmlCommentsFromScriptsAndStyles; bool removeCdataSections = _settings.RemoveCdataSectionsFromScriptsAndStyles; string startPart = string.Empty; string endPart = string.Empty; string newLine = Environment.NewLine; string code = content; string beforeCodeContent = string.Empty; if (isJavaScript) { // Processing of JavaScript code if (_scriptBeginCdataSectionRegex.IsMatch(content)) { beforeCodeContent = _scriptBeginCdataSectionRegex.Match(content).Value; if (!removeCdataSections) { startPart = "//<![CDATA["; endPart = "//]]>"; } code = _scriptBeginCdataSectionRegex.Replace(content, string.Empty); code = _scriptEndCdataSectionRegex.Replace(code, string.Empty); } else if (_scriptBeginMaxCompatibleCdataSectionRegex.IsMatch(content)) { beforeCodeContent = _scriptBeginMaxCompatibleCdataSectionRegex.Match(content).Value; if (!removeCdataSections) { startPart = "<!--//--><![CDATA[//><!--"; endPart = "//--><!]]>"; } code = _scriptBeginMaxCompatibleCdataSectionRegex.Replace(content, string.Empty); code = _scriptEndMaxCompatibleCdataSectionRegex.Replace(code, string.Empty); } else if (_scriptBeginHtmlCommentRegex.IsMatch(content)) { beforeCodeContent = _scriptBeginHtmlCommentRegex.Match(content).Value; if (!removeHtmlComments) { startPart = "<!--"; endPart = "//-->"; } code = _scriptBeginHtmlCommentRegex.Replace(content, string.Empty); code = _scriptEndHtmlCommentRegex.Replace(code, string.Empty); } if (_settings.MinifyEmbeddedJsCode) { CodeMinificationResult minificationResult = _jsMinifier.Minify(code, false); if (minificationResult.Errors.Count == 0) { code = minificationResult.MinifiedContent ?? string.Empty; } if (minificationResult.Errors.Count > 0 || minificationResult.Warnings.Count > 0) { string sourceCode = context.SourceCode; var documentCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( context.NodeCoordinates, beforeCodeContent); foreach (MinificationErrorInfo error in minificationResult.Errors) { var relativeErrorCoordinates = new SourceCodeNodeCoordinates(error.LineNumber, error.ColumnNumber); var absoluteErrorCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( documentCoordinates, 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); var absoluteErrorCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( documentCoordinates, relativeErrorCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( sourceCode, absoluteErrorCoordinates); string message = warning.Message.Trim(); WriteWarning(LogCategoryConstants.JsMinificationWarning, message, _fileContext, absoluteErrorCoordinates.LineNumber, absoluteErrorCoordinates.ColumnNumber, sourceFragment); } } } } else { // Processing of VBScript code if (_scriptBeginCdataSectionRegex.IsMatch(content)) { if (!removeCdataSections) { startPart = "<![CDATA["; endPart = "]]>"; } code = _scriptBeginCdataSectionRegex.Replace(content, string.Empty); code = _scriptEndCdataSectionRegex.Replace(code, string.Empty); } else if (_scriptBeginHtmlCommentRegex.IsMatch(content)) { if (!removeHtmlComments) { startPart = "<!--"; endPart = "-->"; } code = _scriptBeginHtmlCommentRegex.Replace(content, string.Empty); code = _scriptEndHtmlCommentRegex.Replace(code, string.Empty); } } if (minifyWhitespace && code.Length > 0) { code = code.Trim(); } if (startPart.Length > 0 || endPart.Length > 0) { processedScriptContent = string.Concat(startPart, newLine, code, newLine, endPart); } else { processedScriptContent = code; } } else if (_processableScriptTypes.Contains(processedContentType)) { // Processing of JavaScript template GenericHtmlMinifier innerHtmlMinifier = GetInnerHtmlMinifierInstance(); MarkupMinificationResult minificationResult = innerHtmlMinifier.Minify(processedScriptContent, false); if (minificationResult.Errors.Count == 0) { processedScriptContent = minificationResult.MinifiedContent ?? string.Empty; } if (minificationResult.Errors.Count > 0 || minificationResult.Warnings.Count > 0) { string sourceCode = context.SourceCode; var documentCoordinates = context.NodeCoordinates; foreach (MinificationErrorInfo error in minificationResult.Errors) { var relativeErrorCoordinates = new SourceCodeNodeCoordinates(error.LineNumber, error.ColumnNumber); var absoluteErrorCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( documentCoordinates, relativeErrorCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( sourceCode, absoluteErrorCoordinates); string message = error.Message.Trim(); WriteError(LogCategoryConstants.JsTemplateMinificationError, message, _fileContext, absoluteErrorCoordinates.LineNumber, absoluteErrorCoordinates.ColumnNumber, sourceFragment); } foreach (MinificationErrorInfo warning in minificationResult.Warnings) { var relativeErrorCoordinates = new SourceCodeNodeCoordinates(warning.LineNumber, warning.ColumnNumber); var absoluteErrorCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( documentCoordinates, relativeErrorCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( sourceCode, absoluteErrorCoordinates); string message = warning.Message.Trim(); WriteWarning(LogCategoryConstants.JsTemplateMinificationWarning, message, _fileContext, absoluteErrorCoordinates.LineNumber, absoluteErrorCoordinates.ColumnNumber, sourceFragment); } } if (minifyWhitespace && processedScriptContent.Length > 0) { processedScriptContent = processedScriptContent.Trim(); } } return processedScriptContent; }
/// <summary> /// Minify a Knockout binding expression /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="attributeCoordinates">Coordinates of attribute value</param> /// <param name="expressionCoordinates">Coordinates of expression</param> /// <param name="expression">Binding expression</param> /// <returns>Minified binding expression</returns> private string MinifyKnockoutBindingExpression(MarkupParsingContext context, SourceCodeNodeCoordinates attributeCoordinates, SourceCodeNodeCoordinates expressionCoordinates, string expression) { if (string.IsNullOrWhiteSpace(expression)) { return string.Empty; } string result = expression; CrockfordJsMinifier innerCrockfordJsMinifier = GetInnerCrockfordJsMinifierInstance(); CodeMinificationResult minificationResult = innerCrockfordJsMinifier.Minify( JsonHelpers.WrapStringInCurlyBraces(result), true); IList<MinificationErrorInfo> errors = minificationResult.Errors; if (errors.Count == 0) { result = minificationResult.MinifiedContent ?? string.Empty; } else { SourceCodeNodeCoordinates absoluteErrorCoordinates = CalculateAbsoluteInlineCodeErrorCoordinates( context.NodeCoordinates, attributeCoordinates, expressionCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( context.SourceCode, absoluteErrorCoordinates); string errorMessage = errors[0].Message; WriteError(LogCategoryConstants.JsTemplateMinificationError, string.Format(Strings.ErrorMessage_BindingExpressionMinificationFailed, "Knockout", errorMessage.TrimEnd('.')), _fileContext, absoluteErrorCoordinates.LineNumber, absoluteErrorCoordinates.ColumnNumber, sourceFragment); } result = JsonHelpers.UnwrapStringInCurlyBraces(result); return result; }
/// <summary> /// Minify a Knockout binding expression /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="expressionCoordinates">Coordinates of expression</param> /// <param name="expression">Binding expression</param> /// <returns>Minified binding expression</returns> private string MinifyKnockoutBindingExpression(MarkupParsingContext context, SourceCodeNodeCoordinates expressionCoordinates, string expression) { return MinifyKnockoutBindingExpression(context, SourceCodeNodeCoordinates.Empty, expressionCoordinates, expression); }
/// <summary> /// Text handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="text">Text</param> private void TextHandler(MarkupParsingContext context, string text) { XmlNodeType previousNodeType = _currentNodeType; _currentNodeType = XmlNodeType.Text; if (previousNodeType == XmlNodeType.Text) { _currentText = text; _buffer.Add(text); return; } _xmlDeclarationBeforeText = false; _processingInstructionBeforeText = false; _doctypeBeforeText = false; _startTagBeforeText = false; _endTagBeforeText = false; _emptyTagBeforeText = false; switch (previousNodeType) { case XmlNodeType.StartTag: _startTagBeforeText = true; break; case XmlNodeType.EndTag: _endTagBeforeText = true; break; case XmlNodeType.EmptyTag: _emptyTagBeforeText = true; break; case XmlNodeType.XmlDeclaration: _xmlDeclarationBeforeText = true; break; case XmlNodeType.ProcessingInstruction: _processingInstructionBeforeText = true; break; case XmlNodeType.Doctype: _doctypeBeforeText = true; break; } if (_settings.MinifyWhitespace) { if (context.Position == 0) { // Processing starting whitespace text = text.TrimStart(); } else if ((context.Position + text.Length) == context.Length) { // Processing ending whitespace text = text.TrimEnd(); } else if (_xmlDeclarationBeforeText || _processingInstructionBeforeText || _doctypeBeforeText) { // Processing whitespace, that followed after // the XML declaration, processing instruction // or document type declaration if (string.IsNullOrWhiteSpace(text)) { text = string.Empty; } } } _currentText = text; if (text.Length > 0) { _buffer.Add(text); } }
/// <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> /// 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> /// 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; } } }
/// <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> /// Parses XML content /// </summary> /// <param name="content">XML 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); _tagStack = new Stack <StackedXmlTag>(); int endPosition = contentLength - 1; int previousPosition = -1; try { while (_innerContext.Position <= endPosition) { bool isProcessed = false; if (content.CustomStartsWith("<", _innerContext.Position, StringComparison.Ordinal)) { if (content.CustomStartsWith("</", _innerContext.Position, StringComparison.Ordinal)) { // End tag isProcessed = ProcessEndTag(); } else if (content.CustomStartsWith("<!", _innerContext.Position, StringComparison.Ordinal)) { // XML comments isProcessed = ProcessComment(); if (!isProcessed) { // CDATA sections isProcessed = ProcessCdataSection(); } if (!isProcessed) { // Doctype declaration isProcessed = ProcessDoctype(); } } else if (content.CustomStartsWith("<?", _innerContext.Position, StringComparison.Ordinal)) { // XML declaration and processing instructions isProcessed = ProcessProcessingInstruction(); } else { // Start tag isProcessed = ProcessStartTag(); } } if (!isProcessed) { // Text ProcessText(); } if (_innerContext.Position == previousPosition) { throw new XmlParsingException( string.Format(Strings.ErrorMessage_MarkupParsingFailed, "XML"), _innerContext.NodeCoordinates, _innerContext.GetSourceFragment()); } previousPosition = _innerContext.Position; } // Check whether there were not closed tags if (_tagStack.Count > 0) { StackedXmlTag stackedTag = _tagStack.Pop(); throw new XmlParsingException( string.Format(Strings.ErrorMessage_NotClosedTag, stackedTag.Name), stackedTag.Coordinates, SourceCodeNavigator.GetSourceFragment(_innerContext.SourceCode, stackedTag.Coordinates)); } } catch (XmlParsingException) { throw; } finally { _tagStack.Clear(); _context = null; _innerContext = null; } } }
/// <summary> /// Template tags handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="expression">Expression</param> /// <param name="startDelimiter">Start delimiter</param> /// <param name="endDelimiter">End delimiter</param> private void TemplateTagHandler(MarkupParsingContext context, string expression, string startDelimiter, string endDelimiter) { _currentNodeType = HtmlNodeType.TemplateTag; string processedExpression = expression; if (_settings.MinifyAngularBindingExpressions && startDelimiter == "{{" && endDelimiter == "}}") { processedExpression = MinifyAngularBindingExpression(context, expression); } _buffer.Add(startDelimiter); _buffer.Add(processedExpression); _buffer.Add(endDelimiter); }
/// <summary> /// Parses a Angular comment directive /// </summary> /// <param name="commentText">Comment text</param> /// <param name="directiveNameHandler">Directive name handler</param> /// <param name="expressionHandler">Binding expression handler</param> public static void ParseCommentDirective(string commentText, DirectiveNameDelegate directiveNameHandler, ExpressionDelegate expressionHandler) { Match ngCommentDirectiveMatch = _ngCommentDirectiveRegex.Match(commentText); if (ngCommentDirectiveMatch.Success) { var innerContext = new InnerMarkupParsingContext(commentText); var context = new MarkupParsingContext(innerContext); GroupCollection groups = ngCommentDirectiveMatch.Groups; Group directiveNameGroup = groups["directiveName"]; int directiveNamePosition = directiveNameGroup.Index; string originalDirectiveName = directiveNameGroup.Value; string normalizedDirectiveName = NormalizeDirectiveName(originalDirectiveName); innerContext.IncreasePosition(directiveNamePosition); if (directiveNameHandler != null) { directiveNameHandler(context, originalDirectiveName, normalizedDirectiveName); } Group expressionGroup = groups["expression"]; if (expressionGroup.Success) { int expressionPosition = expressionGroup.Index; string expression = expressionGroup.Value.Trim(); innerContext.IncreasePosition(expressionPosition - directiveNamePosition); if (expressionHandler != null) { expressionHandler(context, expression); } } } }
/// <summary> /// Text handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="text">Text</param> private void TextHandler(MarkupParsingContext context, string text) { HtmlNodeType nodeType = _currentNodeType; string tagName; HtmlTagFlags tagFlags; IList<HtmlAttribute> attributes; if (_currentTag != null) { tagName = _currentTag.Name; tagFlags = _currentTag.Flags; attributes = _currentTag.Attributes; } else { tagName = string.Empty; tagFlags = new HtmlTagFlags(); attributes = new List<HtmlAttribute>(); } WhitespaceMinificationMode whitespaceMinificationMode = _settings.WhitespaceMinificationMode; if (nodeType == HtmlNodeType.StartTag && tagFlags.EmbeddedCode) { switch (tagName) { case "script": case "style": string contentType = attributes .Where(a => a.Name == "type") .Select(a => a.Value) .FirstOrDefault() ; if (tagName == "script") { if (string.IsNullOrWhiteSpace(contentType)) { string language = attributes .Where(a => a.Name == "language") .Select(a => a.Value) .FirstOrDefault() ; if (!string.IsNullOrWhiteSpace(language) && language.Trim().ToLowerInvariant() == "vbscript") { contentType = VBS_CONTENT_TYPE; } } text = ProcessEmbeddedScriptContent(context, text, contentType); } else if (tagName == "style") { text = ProcessEmbeddedStyleContent(context, text, contentType); } break; case "svg": text = ProcessEmbeddedSvgContent(context, text); break; case "math": text = ProcessEmbeddedMathMlContent(context, text); break; } } else { if (whitespaceMinificationMode != WhitespaceMinificationMode.None) { if (_tagsWithNotRemovableWhitespaceQueue.Count == 0) { if (context.Position == 0) { // Processing of starting whitespace text = text.TrimStart(); } else if ((context.Position + text.Length) == context.Length) { // Processing of ending whitespace text = text.TrimEnd(); } else if (nodeType == HtmlNodeType.StartTag) { // Processing of whitespace, that followed after the start tag bool allowTrimStart = false; if (tagFlags.Invisible || (tagFlags.NonIndependent && tagFlags.Empty)) { allowTrimStart = true; } else { if (whitespaceMinificationMode == WhitespaceMinificationMode.Medium) { allowTrimStart = tagFlags.Block; } else if (whitespaceMinificationMode == WhitespaceMinificationMode.Aggressive) { allowTrimStart = (tagFlags.Block || ((tagFlags.Inline || tagFlags.InlineBlock) && !tagFlags.Empty)); } } if (allowTrimStart) { text = text.TrimStart(); } } else if (nodeType == HtmlNodeType.EndTag) { // Processing of whitespace, that followed after the end tag bool allowTrimStart = false; if (tagFlags.Invisible || tagFlags.NonIndependent) { allowTrimStart = true; } else { if (whitespaceMinificationMode == WhitespaceMinificationMode.Medium || whitespaceMinificationMode == WhitespaceMinificationMode.Aggressive) { allowTrimStart = tagFlags.Block; } } if (allowTrimStart) { text = text.TrimStart(); } } else if (nodeType == HtmlNodeType.Doctype || nodeType == HtmlNodeType.XmlDeclaration) { // Processing of whitespace, that followed after the document type declaration // or XML declaration text = text.TrimStart(); } if (text.Length > 0) { text = Utils.CollapseWhitespace(text); } } else if (nodeType == HtmlNodeType.StartTag && tagName == "textarea" && string.IsNullOrWhiteSpace(text)) { text = string.Empty; } } } _currentNodeType = HtmlNodeType.Text; _currentText = text; if (text.Length > 0) { _buffer.Add(text); } }
/// <summary> /// CDATA sections handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="cdataText">CDATA text</param> private void CdataSectionHandler(MarkupParsingContext context, string cdataText) { _currentNodeType = XmlNodeType.CdataSection; _buffer.Add("<![CDATA["); _buffer.Add(cdataText); _buffer.Add("]]>"); }
/// <summary> /// XML declaration handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="xmlDeclaration">XML declaration</param> private void XmlDeclarationHandler(MarkupParsingContext context, string xmlDeclaration) { _currentNodeType = HtmlNodeType.XmlDeclaration; WhitespaceMinificationMode whitespaceMinificationMode = _settings.WhitespaceMinificationMode; if (whitespaceMinificationMode != WhitespaceMinificationMode.None) { // Processing of whitespace, that followed before the document type declaration TrimEndLastBufferItem(); } if (_settings.UseXhtmlSyntax) { XmlMinifier innerXmlMinifier = GetInnerXmlMinifierInstance(); MarkupMinificationResult minificationResult = innerXmlMinifier.Minify(xmlDeclaration); if (minificationResult.Errors.Count == 0) { _buffer.Add(minificationResult.MinifiedContent); } else { string sourceCode = context.SourceCode; var documentCoordinates = context.NodeCoordinates; foreach (MinificationErrorInfo error in minificationResult.Errors) { var xmlNodeCoordinates = new SourceCodeNodeCoordinates(error.LineNumber, error.ColumnNumber); var absoluteNodeCoordinates = SourceCodeNavigator.CalculateAbsoluteNodeCoordinates( documentCoordinates, xmlNodeCoordinates); string sourceFragment = SourceCodeNavigator.GetSourceFragment( sourceCode, absoluteNodeCoordinates); string message = Strings.ErrorMessage_XmlDeclarationMinificationFailed; WriteError(LogCategoryConstants.HtmlMinificationError, message, _fileContext, absoluteNodeCoordinates.LineNumber, absoluteNodeCoordinates.ColumnNumber, sourceFragment); } } } else { string sourceCode = context.SourceCode; SourceCodeNodeCoordinates xmlDeclarationCoordinates = context.NodeCoordinates; WriteWarning(LogCategoryConstants.HtmlMinificationWarning, Strings.WarningMessage_XmlDeclarationNotAllowed, _fileContext, xmlDeclarationCoordinates.LineNumber, xmlDeclarationCoordinates.ColumnNumber, SourceCodeNavigator.GetSourceFragment(sourceCode, xmlDeclarationCoordinates)); } }
/// <summary> /// Document type declaration handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="doctype">Document type declaration</param> private void DoctypeDelegateHandler(MarkupParsingContext context, string doctype) { _currentNodeType = XmlNodeType.Doctype; if (_settings.MinifyWhitespace) { RemoveLastWhitespaceBufferItems(); } _buffer.Add(Utils.CollapseWhitespace(doctype)); }
/// <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> /// End tags handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="tagName">Tag name</param> private void EndTagHandler(MarkupParsingContext context, string tagName) { XmlNodeType previousNodeType = _currentNodeType; string previousText = _currentText; _currentNodeType = XmlNodeType.EndTag; _currentText = string.Empty; if (_settings.CollapseTagsWithoutContent && previousNodeType == XmlNodeType.StartTag && previousText.Length == 0) { if (TransformLastStartTagToEmptyTag()) { FlushBuffer(); return; } } if (_settings.MinifyWhitespace && previousNodeType == XmlNodeType.Text && (_endTagBeforeText || _emptyTagBeforeText)) { RemoveLastWhitespaceBufferItems(); } // Add end tag to buffer _buffer.Add("</"); _buffer.Add(tagName); _buffer.Add(">"); FlushBuffer(); }
/// <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> /// Start tags handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="tagName">Tag name</param> /// <param name="attributes">List of attributes</param> private void StartTagHandler(MarkupParsingContext context, string tagName, IList<XmlAttribute> attributes) { XmlNodeType previousNodeType = _currentNodeType; _currentNodeType = XmlNodeType.StartTag; _currentText = string.Empty; if (_settings.MinifyWhitespace && previousNodeType == XmlNodeType.Text && (_startTagBeforeText || _endTagBeforeText || _emptyTagBeforeText || _xmlDeclarationBeforeText || _processingInstructionBeforeText || _doctypeBeforeText)) { RemoveLastWhitespaceBufferItems(); } _buffer.Add("<"); _buffer.Add(tagName); RenderAttributes(attributes); _buffer.Add(">"); }
/// <summary> /// Ignored fragments handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="fragment">Ignored fragment</param> private void IgnoredFragmentHandler(MarkupParsingContext context, string fragment) { _currentNodeType = XmlNodeType.IgnoredFragment; if (_settings.MinifyWhitespace) { RemoveLastWhitespaceBufferItems(); } if (fragment.Length > 0) { _buffer.Add(fragment); } }
/// <summary> /// XML declaration handler /// </summary> /// <param name="context">Markup parsing context</param> /// <param name="attributes">List of attributes</param> private void XmlDeclarationHandler(MarkupParsingContext context, IList<XmlAttribute> attributes) { _currentNodeType = XmlNodeType.XmlDeclaration; if (_settings.MinifyWhitespace) { RemoveLastWhitespaceBufferItems(); } _buffer.Add("<?xml"); RenderAttributes(attributes); _buffer.Add("?>"); }
/// <summary> /// Parses XML content /// </summary> /// <param name="content">XML 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); _tagStack = new Stack<StackedXmlTag>(); int endPosition = contentLength - 1; int previousPosition = -1; try { while (_innerContext.Position <= endPosition) { bool isProcessed = false; if (content.CustomStartsWith("<", _innerContext.Position, StringComparison.Ordinal)) { if (content.CustomStartsWith("</", _innerContext.Position, StringComparison.Ordinal)) { // End tag isProcessed = ProcessEndTag(); } else if (content.CustomStartsWith("<!", _innerContext.Position, StringComparison.Ordinal)) { // XML comments isProcessed = ProcessComment(); if (!isProcessed) { // CDATA sections isProcessed = ProcessCdataSection(); } if (!isProcessed) { // Doctype declaration isProcessed = ProcessDoctype(); } } else if (content.CustomStartsWith("<?", _innerContext.Position, StringComparison.Ordinal)) { // XML declaration and processing instructions isProcessed = ProcessProcessingInstruction(); } else { // Start tag isProcessed = ProcessStartTag(); } } if (!isProcessed) { // Text ProcessText(); } if (_innerContext.Position == previousPosition) { throw new XmlParsingException( string.Format(Strings.ErrorMessage_MarkupParsingFailed, "XML"), _innerContext.NodeCoordinates, _innerContext.GetSourceFragment()); } previousPosition = _innerContext.Position; } // Check whether there were not closed tags if (_tagStack.Count > 0) { StackedXmlTag stackedTag = _tagStack.Pop(); throw new XmlParsingException( string.Format(Strings.ErrorMessage_NotClosedTag, stackedTag.Name), stackedTag.Coordinates, SourceCodeNavigator.GetSourceFragment(_innerContext.SourceCode, stackedTag.Coordinates)); } } catch (XmlParsingException) { throw; } finally { _tagStack.Clear(); _context = null; _innerContext = null; } } }