diff --git a/HtmlMinifier.Tests/ArgumentsTests.cs b/HtmlMinifier.Tests/ArgumentsTests.cs index b89d69e..067ebac 100644 --- a/HtmlMinifier.Tests/ArgumentsTests.cs +++ b/HtmlMinifier.Tests/ArgumentsTests.cs @@ -15,7 +15,7 @@ public void FindValuesInArgs_WithIgnores_ShouldReturnCorrectly() argsList.Add("ignorejscomments"); // Act - Features disabledFeatures = Program.FindValuesInArgs(argsList.ToArray()); + Features disabledFeatures = new Features(argsList.ToArray()); // Assert Assert.That(disabledFeatures.IgnoreHtmlComments, Is.True); @@ -30,7 +30,7 @@ public void FindValuesInArgs_WithOneIgnore_ShouldReturnCorrectly() argsList.Add("ignorehtmlcomments"); // Act - Features disabledFeatures = Program.FindValuesInArgs(argsList.ToArray()); + Features disabledFeatures = new Features(argsList.ToArray()); // Assert Assert.That(disabledFeatures.IgnoreHtmlComments, Is.True); diff --git a/HtmlMinifier.Tests/FileExtensionTests.cs b/HtmlMinifier.Tests/FileExtensionTests.cs new file mode 100644 index 0000000..3652373 --- /dev/null +++ b/HtmlMinifier.Tests/FileExtensionTests.cs @@ -0,0 +1,19 @@ +using NUnit.Framework; +using System.Collections.Generic; + +namespace HtmlMinifier.Tests +{ + [TestFixture] + public class FileExtensionTests + { + [Test] + public void GithubIssue25__ShouldReturnCorrectly() + { + Assert.That("test.html".IsHtmlFile(), Is.True); + Assert.That("codes.js.aspx".IsHtmlFile(), Is.True); + + Assert.That("codes.aspx.js".IsHtmlFile(), Is.False); + Assert.That("aspx.codes.js".IsHtmlFile(), Is.False); + } + } +} diff --git a/HtmlMinifier.Tests/HtmlMinifier.Tests.csproj b/HtmlMinifier.Tests/HtmlMinifier.Tests.csproj index 1db358b..c857a64 100644 --- a/HtmlMinifier.Tests/HtmlMinifier.Tests.csproj +++ b/HtmlMinifier.Tests/HtmlMinifier.Tests.csproj @@ -44,6 +44,7 @@ + @@ -75,6 +76,9 @@ + + + ", ""); + return reader.MinifyHtmlCode(features); } - - // single-line doctype must be preserved - var firstEndBracketPosition = htmlContents.IndexOf(">", StringComparison.Ordinal); - if (firstEndBracketPosition >= 0) - { - htmlContents = htmlContents.Remove(firstEndBracketPosition, 1); - htmlContents = htmlContents.Insert(firstEndBracketPosition, ">"); - } - - return htmlContents.Trim(); - } - - /// - /// Removes any JavaScript Comments in a script block - /// - /// - /// A string with all JS comments removed - public static string RemoveJavaScriptComments(string javaScriptComments) - { - // Remove JavaScript comments - Regex extractScripts = new Regex(@"]*>[\s\S]*?"); - - // Loop through the script blocks - foreach (Match match in extractScripts.Matches(javaScriptComments)) - { - var scriptBlock = match.Value; - - javaScriptComments = javaScriptComments.Replace(scriptBlock, Regex.Replace(scriptBlock, @"[^:|""|']//(.*?)\r?\n", "")); - - } - - return javaScriptComments; - } - - /// - /// Ensure that the max character count is less than 65K. - /// If so, break onto the next line. - /// - /// The minified HTML - /// An optional parameter for the max character count - /// A html string - public static string EnsureMaxLength(string htmlContents, string[] args) - { - int maxLength = 60000; - - // This is a check to see if the args contain an optional parameter for the max line length - if (args != null && args.Length > 1) - { - // Try and parse the value sent through - if (!int.TryParse(args[1], out maxLength)) - { - maxLength = 60000; - } - - int htmlLength = htmlContents.Length; - int currentMaxLength = maxLength; - int position; - - while (htmlLength > currentMaxLength) - { - position = htmlContents.LastIndexOf("><", currentMaxLength); - htmlContents = htmlContents.Substring(0, position + 1) + "\r\n" + htmlContents.Substring(position + 1); - currentMaxLength += maxLength; - } - } - - return htmlContents; - } - - /// - /// Check the arguments passed in to determine if we should enable or disable any features. - /// - /// The arguments passed in. - /// A list of features to be enabled or disabled. - public static Features FindValuesInArgs(string[] args) - { - _features = new Features(); - - if (args.Contains("ignorehtmlcomments")) - { - _features.IgnoreHtmlComments = true; - } - - if (args.Contains("ignorejscomments")) - { - _features.IgnoreJsComments = true; - } - - return _features; } } } diff --git a/ViewMinifier/StreamReaderExtension.cs b/ViewMinifier/StreamReaderExtension.cs new file mode 100644 index 0000000..53d9a91 --- /dev/null +++ b/ViewMinifier/StreamReaderExtension.cs @@ -0,0 +1,210 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace HtmlMinifier +{ + public static class StreamReaderExtension + { + public static string MinifyHtmlCode(this StreamReader reader, Features features) + { + return MinifyHtmlCode(reader.ReadToEnd(), features); + } + + public static string MinifyHtmlCode(string htmlCode, Features features) + { + string contents; + // Minify the contents + contents = MinifyHtml(htmlCode, features); + + // Ensure that the max length is less than 65K characters + contents = EnsureMaxLength(contents, features); + + // Re-add the @model declaration + contents = ReArrangeDeclarations(contents); + return contents; + } + + /// + /// Find any occurences of the particular Razor keywords + /// and add a new line or move to the top of the view. + /// + /// The contents of the file + /// + /// The . + /// + public static string ReArrangeDeclarations(string fileContents) + { + // A list of all the declarations + Dictionary declarations = new Dictionary(); + declarations.Add("@model ", true); + declarations.Add("@using ", false); + declarations.Add("@inherits ", false); + + // Loop through the declarations + foreach (var declaration in declarations) + { + fileContents = ReArrangeDeclaration(fileContents, declaration.Key, declaration.Value); + } + + return fileContents; + } + + /// + /// Re-arranges the razor syntax on its own line. + /// It seems to break the razor engine if this isnt on + /// it's own line in certain cases. + /// + /// The file contents. + /// The declaration keywords that will cause a new line split. + /// + /// The . + /// + private static string ReArrangeDeclaration(string fileContents, string declaration, bool bringToTop) + { + // Find possible multiple occurences in the file contents + MatchCollection matches = Regex.Matches(fileContents, declaration); + + // Loop through the matches + int alreadyMatched = 0; + foreach (Match match in matches) + { + int position = declaration.Length; + int declarationPosition = match.Index; + + // If we have more than one match, we need to keep the counter moving everytime we add a new line + if (matches.Count > 1 && alreadyMatched > 0) + { + // Cos we added one or more new line break \n\r + declarationPosition += (2 * alreadyMatched); + } + + while (declarationPosition >= 0) + { + // Move one forward + position += 1; + string substring = fileContents.Substring(declarationPosition, position); + + // Check if it contains a whitespace at the end + if (substring.EndsWith(" ") || substring.EndsWith(">")) + { + if (bringToTop) + { + // First replace the occurence + fileContents = fileContents.Replace(substring, ""); + + // Next move it to the top on its own line + fileContents = substring + Environment.NewLine + fileContents; + break; + } + else + { + // Add a line break afterwards + fileContents = fileContents.Replace(substring, substring + Environment.NewLine); + alreadyMatched++; + break; + } + } + } + } + + return fileContents; + } + + /// + /// Minifies the given HTML string. + /// + /// The html to minify. + /// The features + /// + /// The . + /// + public static string MinifyHtml(string htmlContents, Features features) + { + // First, remove all JavaScript comments + if (!features.IgnoreJsComments) + { + htmlContents = RemoveJavaScriptComments(htmlContents); + } + + // Minify the string + htmlContents = Regex.Replace(htmlContents, @"/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/", ""); + + // Replace line comments + htmlContents = Regex.Replace(htmlContents, @"// (.*?)\r?\n", "", RegexOptions.Singleline); + + // Replace spaces between quotes + htmlContents = Regex.Replace(htmlContents, @"\s+", " "); + + // Replace line breaks + htmlContents = Regex.Replace(htmlContents, @"\s*\n\s*", "\n"); + + // Replace spaces between brackets + htmlContents = Regex.Replace(htmlContents, @"\s*\>\s*\<\s*", "><"); + + // Replace comments + if (!features.IgnoreHtmlComments) + { + htmlContents = Regex.Replace(htmlContents, @"", ""); + } + + // single-line doctype must be preserved + var firstEndBracketPosition = htmlContents.IndexOf(">", StringComparison.Ordinal); + if (firstEndBracketPosition >= 0) + { + htmlContents = htmlContents.Remove(firstEndBracketPosition, 1); + htmlContents = htmlContents.Insert(firstEndBracketPosition, ">"); + } + + return htmlContents.Trim(); + } + + /// + /// Removes any JavaScript Comments in a script block + /// + /// + /// A string with all JS comments removed + public static string RemoveJavaScriptComments(string javaScriptComments) + { + // Remove JavaScript comments + Regex extractScripts = new Regex(@"]*>[\s\S]*?"); + + // Loop through the script blocks + foreach (Match match in extractScripts.Matches(javaScriptComments)) + { + var scriptBlock = match.Value; + + javaScriptComments = javaScriptComments.Replace(scriptBlock, Regex.Replace(scriptBlock, @"[^:|""|']//(.*?)\r?\n", "")); + + } + + return javaScriptComments; + } + + /// + /// Ensure that the max character count is less than 65K. + /// If so, break onto the next line. + /// + /// The minified HTML + /// The features + /// A html string + public static string EnsureMaxLength(string htmlContents, Features features) + { + if (features.MaxLength > 0) + { + int htmlLength = htmlContents.Length; + int currentMaxLength = features.MaxLength; + int position; + + while (htmlLength > currentMaxLength) + { + position = htmlContents.LastIndexOf("><", currentMaxLength); + htmlContents = htmlContents.Substring(0, position + 1) + "\r\n" + htmlContents.Substring(position + 1); + currentMaxLength += features.MaxLength; + } + } + return htmlContents; + } + } +} diff --git a/ViewMinifier/StringExtension.cs b/ViewMinifier/StringExtension.cs new file mode 100644 index 0000000..a65b3e9 --- /dev/null +++ b/ViewMinifier/StringExtension.cs @@ -0,0 +1,17 @@ +namespace HtmlMinifier +{ + public static class StringExtension + { + public static bool IsHtmlFile(this string value) + { + var file = value.ToLower(); + return file.EndsWith(".cshtml") || + file.EndsWith(".vbhtml") || + file.EndsWith(".aspx") || + file.EndsWith(".html") || + file.EndsWith(".htm") || + file.EndsWith(".ascx") || + file.EndsWith(".master"); + } + } +}