From 71e1b6d9f1440ad716c5b6d5203ebde918b4ba60 Mon Sep 17 00:00:00 2001 From: Mads Storm Date: Thu, 9 Feb 2012 22:27:30 +0100 Subject: [PATCH 1/8] Async/Defer transform --- .../Module/ResponseTransformerFacts.cs | 30 ++++++++++++ RequestReduce.Facts/RRContainerFacts.cs | 7 ++- RequestReduce/Module/ResponseTransformer.cs | 30 ++++++++---- RequestReduce/ResourceTypes/CssResource.cs | 7 ++- RequestReduce/ResourceTypes/IResourceType.cs | 3 +- .../ResourceTypes/JavaScriptResource.cs | 46 +++++++++++++++++-- 6 files changed, 107 insertions(+), 16 deletions(-) diff --git a/RequestReduce.Facts/Module/ResponseTransformerFacts.cs b/RequestReduce.Facts/Module/ResponseTransformerFacts.cs index aa80e5a..4ef3c91 100644 --- a/RequestReduce.Facts/Module/ResponseTransformerFacts.cs +++ b/RequestReduce.Facts/Module/ResponseTransformerFacts.cs @@ -778,6 +778,36 @@ public void WillTransformHeadWithDuplicateScriptsAndInlineScript() RRContainer.Current = null; } + + [Fact] + public void WillBundleAsyncAndDeferScriptsSeparately() + { + var testable = new TestableResponseTransformer(); + var transform = @" + + + + + + + +site + "; + var transformed = @" + +site + "; + testable.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::")).Returns("http://server/Me7.js"); + testable.Mock().Setup(x => x.FindReduction("http://server/Me3.js::http://server/Me4.js::")).Returns("http://server/Me8.js"); + testable.Mock().Setup(x => x.FindReduction("http://server/Me5.js::http://server/Me6.js::")).Returns("http://server/Me9.js"); + testable.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + var result = testable.ClassUnderTest.Transform(transform); + + Assert.Equal(transformed, result); + } + + } } } diff --git a/RequestReduce.Facts/RRContainerFacts.cs b/RequestReduce.Facts/RRContainerFacts.cs index bef0856..a9c5f38 100644 --- a/RequestReduce.Facts/RRContainerFacts.cs +++ b/RequestReduce.Facts/RRContainerFacts.cs @@ -29,7 +29,12 @@ public System.Collections.Generic.IEnumerable SupportedMimeTypes get { throw new System.NotImplementedException(); } } - public string TransformedMarkupTag(string url) + public int Bundle(string resource) + { + throw new System.NotImplementedException(); + } + + public string TransformedMarkupTag(string url, int bundle) { throw new System.NotImplementedException(); } diff --git a/RequestReduce/Module/ResponseTransformer.cs b/RequestReduce/Module/ResponseTransformer.cs index 63b932b..a9d5fe7 100644 --- a/RequestReduce/Module/ResponseTransformer.cs +++ b/RequestReduce/Module/ResponseTransformer.cs @@ -51,8 +51,10 @@ private string Transform(string preTransform) where T : IResourceType { var urls = new StringBuilder(); var transformableMatches = new List(); - foreach (var match in matches) + int bundle = 0; + for (int cursor = 0; cursor < matches.Count; cursor++) { + var match = matches[cursor]; var strMatch = match.ToString(); var urlMatch = Regex.UrlPattern.Match(strMatch); bool matched = false; @@ -61,23 +63,31 @@ private string Transform(string preTransform) where T : IResourceType var url = RelativeToAbsoluteUtility.ToAbsolute(config.BaseAddress == null ? context.Request.Url : new Uri(config.BaseAddress), urlMatch.Groups["url"].Value); if ((resource.TagValidator == null || resource.TagValidator(strMatch, url)) && (RRContainer.Current.GetAllInstances().Where(x => (x is CssFilter && typeof(T) == typeof(CssResource)) || (x is JavascriptFilter && typeof(T) == typeof(JavaScriptResource))).FirstOrDefault(y => y.IgnoreTarget(new CssJsFilterContext(context.Request, url, strMatch))) == null)) { - matched = true; - urls.Append(url); - urls.Append(GetMedia(strMatch)); - urls.Append("::"); - transformableMatches.Add(strMatch); + if ((transformableMatches.Count == 0) || (resource.Bundle(strMatch) == bundle)) + { + matched = true; + bundle = resource.Bundle(strMatch); + urls.Append(url); + urls.Append(GetMedia(strMatch)); + urls.Append("::"); + transformableMatches.Add(strMatch); + } + else + { + cursor--; // This resource into next bundle + } } } if (!matched && transformableMatches.Count > 0) { - preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform); + preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform, bundle); urls.Length = 0; transformableMatches.Clear(); } } if (transformableMatches.Count > 0) { - preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform); + preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform, bundle); urls.Length = 0; transformableMatches.Clear(); } @@ -93,7 +103,7 @@ private string GetMedia(string strMatch) return null; } - private string DoTransform(string preTransform, StringBuilder urls, List transformableMatches, string noCommentTransform) where T : IResourceType + private string DoTransform(string preTransform, StringBuilder urls, List transformableMatches, string noCommentTransform, int bundle) where T : IResourceType { var resource = RRContainer.Current.GetInstance(); RRTracer.Trace("Looking for reduction for {0}", urls); @@ -110,7 +120,7 @@ private string DoTransform(string preTransform, StringBuilder urls, List SupportedMimeTypes get { return new[] { "text/css" }; } } - public string TransformedMarkupTag(string url) + public int Bundle(string resource) + { + return 0; + } + + public string TransformedMarkupTag(string url, int bundle) { return string.Format(CssFormat, url); } diff --git a/RequestReduce/ResourceTypes/IResourceType.cs b/RequestReduce/ResourceTypes/IResourceType.cs index 4e7de27..cd5a53b 100644 --- a/RequestReduce/ResourceTypes/IResourceType.cs +++ b/RequestReduce/ResourceTypes/IResourceType.cs @@ -8,7 +8,8 @@ public interface IResourceType { string FileName { get; } IEnumerable SupportedMimeTypes { get; } - string TransformedMarkupTag(string url); + int Bundle(string resource); + string TransformedMarkupTag(string url, int bundle); Regex ResourceRegex { get; } Func TagValidator { get; set; } } diff --git a/RequestReduce/ResourceTypes/JavaScriptResource.cs b/RequestReduce/ResourceTypes/JavaScriptResource.cs index ff041fd..1561cea 100644 --- a/RequestReduce/ResourceTypes/JavaScriptResource.cs +++ b/RequestReduce/ResourceTypes/JavaScriptResource.cs @@ -9,9 +9,26 @@ namespace RequestReduce.ResourceTypes { public class JavaScriptResource : IResourceType { - private const string ScriptFormat = @""; + private enum ScriptBundle + { + Default = 0, + Async = 1, + Defer = 2, + AsyncDefer = 3 + } + + private readonly string[] ScriptFormats = new string[Enum.GetValues(typeof(ScriptBundle)).Length]; private readonly Regex scriptPattern = new Regex(@"|)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); private static readonly Regex ScriptFilterPattern = new Regex(@"^]+src=[^>]+>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); + + public JavaScriptResource() + { + ScriptFormats[(int)ScriptBundle.Default] = @""; + ScriptFormats[(int)ScriptBundle.Async] = @""; + ScriptFormats[(int)ScriptBundle.Defer] = @""; + ScriptFormats[(int)ScriptBundle.AsyncDefer] = @""; + } + private Func tagValidator = ((tag, url) => { var match = ScriptFilterPattern.Match(tag); @@ -31,9 +48,32 @@ public IEnumerable SupportedMimeTypes get { return new[] { "text/javascript", "application/javascript", "application/x-javascript" }; } } - public string TransformedMarkupTag(string url) + public int Bundle(string resource) + { + // Some real Regex needed here !!! + // Some real Regex needed here !!! + + if(resource.Contains("async") && resource.Contains("defer")) + { + return (int)ScriptBundle.AsyncDefer; + } + + if (resource.Contains("async") && !resource.Contains("defer")) + { + return (int)ScriptBundle.Async; + } + + if (resource.Contains("defer") && !resource.Contains("async")) + { + return (int)ScriptBundle.Defer; + } + + return (int)ScriptBundle.Default; + } + + public string TransformedMarkupTag(string url, int bundle) { - return string.Format(ScriptFormat, url); + return string.Format(ScriptFormats[bundle], url); } public Regex ResourceRegex From 802eaf95636225c98e13b398905d7b7d876d6218 Mon Sep 17 00:00:00 2001 From: Mads Storm Date: Sun, 12 Feb 2012 02:13:15 +0100 Subject: [PATCH 2/8] Bundle Async/Defer scripts at end of page. --- RequestReduce.Facts/RRContainerFacts.cs | 6 ++ RequestReduce/Module/ResponseFilter.cs | 11 +++ RequestReduce/Module/ResponseTransformer.cs | 86 +++++++++++++++++-- RequestReduce/ResourceTypes/CssResource.cs | 6 ++ RequestReduce/ResourceTypes/IResourceType.cs | 1 + .../ResourceTypes/JavaScriptResource.cs | 22 ++--- 6 files changed, 114 insertions(+), 18 deletions(-) diff --git a/RequestReduce.Facts/RRContainerFacts.cs b/RequestReduce.Facts/RRContainerFacts.cs index a9c5f38..2fa7491 100644 --- a/RequestReduce.Facts/RRContainerFacts.cs +++ b/RequestReduce.Facts/RRContainerFacts.cs @@ -46,6 +46,12 @@ public System.Text.RegularExpressions.Regex ResourceRegex public System.Func TagValidator { get; set; } + + + public bool IsDeferred(int bundle) + { + throw new NotImplementedException(); + } } [Fact] diff --git a/RequestReduce/Module/ResponseFilter.cs b/RequestReduce/Module/ResponseFilter.cs index 4769537..3a16424 100644 --- a/RequestReduce/Module/ResponseFilter.cs +++ b/RequestReduce/Module/ResponseFilter.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using RequestReduce.Api; +using System.Web; namespace RequestReduce.Module { @@ -89,6 +90,7 @@ public override bool CanWrite public override void Close() { + WriteDeferredBundles(); Closed = true; BaseStream.Close(); } @@ -102,6 +104,7 @@ public override void Flush() BaseStream.Write(transformed, 0, transformed.Length); transformBuffer.Clear(); } + BaseStream.Flush(); RRTracer.Trace("Flushing Filter"); } @@ -415,5 +418,13 @@ public override long Position throw new NotSupportedException(); } } + + + private void WriteDeferredBundles() + { + string deferred = responseTransformer.EmptyDeferredBundles(); + byte[] buffer = encoding.GetBytes(deferred); + BaseStream.Write(buffer, 0, buffer.Length); + } } } diff --git a/RequestReduce/Module/ResponseTransformer.cs b/RequestReduce/Module/ResponseTransformer.cs index a9d5fe7..9059945 100644 --- a/RequestReduce/Module/ResponseTransformer.cs +++ b/RequestReduce/Module/ResponseTransformer.cs @@ -14,6 +14,7 @@ namespace RequestReduce.Module public interface IResponseTransformer { string Transform(string preTransform); + string EmptyDeferredBundles(); } public class ResponseTransformer : IResponseTransformer @@ -24,6 +25,7 @@ public class ResponseTransformer : IResponseTransformer private static readonly RegexCache Regex = new RegexCache(); private readonly IReducingQueue reducingQueue; private readonly HttpContextBase context; + private readonly Dictionary>>> deferredResources = new Dictionary>>>(); public ResponseTransformer(IReductionRepository reductionRepository, IReducingQueue reducingQueue, HttpContextBase context, IRRConfiguration config, IUriBuilder uriBuilder) { @@ -63,18 +65,41 @@ private string Transform(string preTransform) where T : IResourceType var url = RelativeToAbsoluteUtility.ToAbsolute(config.BaseAddress == null ? context.Request.Url : new Uri(config.BaseAddress), urlMatch.Groups["url"].Value); if ((resource.TagValidator == null || resource.TagValidator(strMatch, url)) && (RRContainer.Current.GetAllInstances().Where(x => (x is CssFilter && typeof(T) == typeof(CssResource)) || (x is JavascriptFilter && typeof(T) == typeof(JavaScriptResource))).FirstOrDefault(y => y.IgnoreTarget(new CssJsFilterContext(context.Request, url, strMatch))) == null)) { - if ((transformableMatches.Count == 0) || (resource.Bundle(strMatch) == bundle)) + int matchBundle = resource.Bundle(strMatch); + if(!resource.IsDeferred(matchBundle)) { - matched = true; - bundle = resource.Bundle(strMatch); - urls.Append(url); - urls.Append(GetMedia(strMatch)); - urls.Append("::"); - transformableMatches.Add(strMatch); + if ((transformableMatches.Count == 0) || (matchBundle == bundle)) + { + matched = true; + bundle = resource.Bundle(strMatch); + urls.Append(url); + urls.Append(GetMedia(strMatch)); + urls.Append("::"); + transformableMatches.Add(strMatch); + } + else + { + cursor--; // This resource into next bundle + } } else { - cursor--; // This resource into next bundle + // Removed deferred resource + var idx = preTransform.IndexOf(strMatch, StringComparison.Ordinal); + preTransform = preTransform.Remove(idx, strMatch.Length); + + // Add to deferred resource bundle + if(!deferredResources.ContainsKey(resource)) + { + deferredResources[resource] = new Dictionary>>(); + } + var deferredResourceBundles = deferredResources[resource]; + if(!deferredResourceBundles.ContainsKey(matchBundle)) + { + deferredResourceBundles[matchBundle] = new List>(); + } + var deferredBundle = deferredResourceBundles[matchBundle]; + deferredBundle.Add(new KeyValuePair(url, strMatch)); } } } @@ -140,5 +165,50 @@ private string DoTransform(string preTransform, StringBuilder urls, List(); + } + + public string EmptyDeferredResourceBundle() where T : IResourceType + { + var resource = RRContainer.Current.GetInstance(); + StringBuilder completeTransform = new StringBuilder(); + + if (deferredResources.ContainsKey(resource)) + { + var bundles = deferredResources[resource]; + foreach (int bundle in bundles.Keys) + { + if (bundles.ContainsKey(bundle)) + { + var urls = new StringBuilder(); + var transformableMatches = new List(); + StringBuilder transformBuilder = new StringBuilder(); + + var matches = bundles[bundle]; + foreach (var match in matches) + { + string url = match.Key; + string strMatch = match.Value; + urls.Append(url); + urls.Append(GetMedia(strMatch)); + urls.Append("::"); + transformableMatches.Add(strMatch); + transformBuilder.Append(strMatch); + } + + string transform = transformBuilder.ToString(); + var noCommentTransform = Regex.HtmlCommentPattern.Replace(transform, string.Empty); + transform = DoTransform(transform, urls, transformableMatches, noCommentTransform, bundle); + completeTransform.Append(transform); + } + } + } + return completeTransform.ToString(); + } + } } diff --git a/RequestReduce/ResourceTypes/CssResource.cs b/RequestReduce/ResourceTypes/CssResource.cs index 7b69bc2..ccfb0e2 100644 --- a/RequestReduce/ResourceTypes/CssResource.cs +++ b/RequestReduce/ResourceTypes/CssResource.cs @@ -36,5 +36,11 @@ public Regex ResourceRegex public Func TagValidator { get; set; } + + + public bool IsDeferred(int bundle) + { + return false; + } } } diff --git a/RequestReduce/ResourceTypes/IResourceType.cs b/RequestReduce/ResourceTypes/IResourceType.cs index cd5a53b..78cc8e2 100644 --- a/RequestReduce/ResourceTypes/IResourceType.cs +++ b/RequestReduce/ResourceTypes/IResourceType.cs @@ -9,6 +9,7 @@ public interface IResourceType string FileName { get; } IEnumerable SupportedMimeTypes { get; } int Bundle(string resource); + bool IsDeferred(int bundle); string TransformedMarkupTag(string url, int bundle); Regex ResourceRegex { get; } Func TagValidator { get; set; } diff --git a/RequestReduce/ResourceTypes/JavaScriptResource.cs b/RequestReduce/ResourceTypes/JavaScriptResource.cs index 1561cea..079f68b 100644 --- a/RequestReduce/ResourceTypes/JavaScriptResource.cs +++ b/RequestReduce/ResourceTypes/JavaScriptResource.cs @@ -13,8 +13,7 @@ private enum ScriptBundle { Default = 0, Async = 1, - Defer = 2, - AsyncDefer = 3 + Defer = 2 } private readonly string[] ScriptFormats = new string[Enum.GetValues(typeof(ScriptBundle)).Length]; @@ -26,7 +25,6 @@ public JavaScriptResource() ScriptFormats[(int)ScriptBundle.Default] = @""; ScriptFormats[(int)ScriptBundle.Async] = @""; ScriptFormats[(int)ScriptBundle.Defer] = @""; - ScriptFormats[(int)ScriptBundle.AsyncDefer] = @""; } private Func tagValidator = ((tag, url) => @@ -53,17 +51,12 @@ public int Bundle(string resource) // Some real Regex needed here !!! // Some real Regex needed here !!! - if(resource.Contains("async") && resource.Contains("defer")) - { - return (int)ScriptBundle.AsyncDefer; - } - - if (resource.Contains("async") && !resource.Contains("defer")) + if (resource.Contains("async")) { return (int)ScriptBundle.Async; } - if (resource.Contains("defer") && !resource.Contains("async")) + if (resource.Contains("defer")) { return (int)ScriptBundle.Defer; } @@ -93,5 +86,14 @@ public Func TagValidator tagValidator = value; } } + + public bool IsDeferred(int bundle) + { + if(bundle == (int)ScriptBundle.Async || bundle == (int)ScriptBundle.Defer) + { + return true; + } + return false; + } } } From 377d1532adce8d2d4a746e39c472ccd118d661fd Mon Sep 17 00:00:00 2001 From: Mads Storm Date: Sun, 12 Feb 2012 11:35:39 +0100 Subject: [PATCH 3/8] Dynamic script load for async bundle --- RequestReduce.Facts/RRContainerFacts.cs | 8 +++++- RequestReduce/Module/ResponseTransformer.cs | 26 ++++++++++++------- RequestReduce/ResourceTypes/CssResource.cs | 7 ++++- RequestReduce/ResourceTypes/IResourceType.cs | 3 ++- .../ResourceTypes/JavaScriptResource.cs | 18 ++++++++++--- 5 files changed, 47 insertions(+), 15 deletions(-) diff --git a/RequestReduce.Facts/RRContainerFacts.cs b/RequestReduce.Facts/RRContainerFacts.cs index 2fa7491..2154e00 100644 --- a/RequestReduce.Facts/RRContainerFacts.cs +++ b/RequestReduce.Facts/RRContainerFacts.cs @@ -48,7 +48,13 @@ public System.Text.RegularExpressions.Regex ResourceRegex public System.Func TagValidator { get; set; } - public bool IsDeferred(int bundle) + public bool IsLoadDeferred(int bundle) + { + throw new NotImplementedException(); + } + + + public bool IsDynamicLoad(int bundle) { throw new NotImplementedException(); } diff --git a/RequestReduce/Module/ResponseTransformer.cs b/RequestReduce/Module/ResponseTransformer.cs index 9059945..134b498 100644 --- a/RequestReduce/Module/ResponseTransformer.cs +++ b/RequestReduce/Module/ResponseTransformer.cs @@ -53,7 +53,7 @@ private string Transform(string preTransform) where T : IResourceType { var urls = new StringBuilder(); var transformableMatches = new List(); - int bundle = 0; + int currentBundle = 0; for (int cursor = 0; cursor < matches.Count; cursor++) { var match = matches[cursor]; @@ -66,12 +66,12 @@ private string Transform(string preTransform) where T : IResourceType if ((resource.TagValidator == null || resource.TagValidator(strMatch, url)) && (RRContainer.Current.GetAllInstances().Where(x => (x is CssFilter && typeof(T) == typeof(CssResource)) || (x is JavascriptFilter && typeof(T) == typeof(JavaScriptResource))).FirstOrDefault(y => y.IgnoreTarget(new CssJsFilterContext(context.Request, url, strMatch))) == null)) { int matchBundle = resource.Bundle(strMatch); - if(!resource.IsDeferred(matchBundle)) + if(!resource.IsLoadDeferred(matchBundle)) { - if ((transformableMatches.Count == 0) || (matchBundle == bundle)) + if ((transformableMatches.Count == 0) || (matchBundle == currentBundle)) { matched = true; - bundle = resource.Bundle(strMatch); + currentBundle = resource.Bundle(strMatch); urls.Append(url); urls.Append(GetMedia(strMatch)); urls.Append("::"); @@ -84,11 +84,11 @@ private string Transform(string preTransform) where T : IResourceType } else { - // Removed deferred resource + // This resource into deferred bundle + var idx = preTransform.IndexOf(strMatch, StringComparison.Ordinal); preTransform = preTransform.Remove(idx, strMatch.Length); - // Add to deferred resource bundle if(!deferredResources.ContainsKey(resource)) { deferredResources[resource] = new Dictionary>>(); @@ -105,14 +105,14 @@ private string Transform(string preTransform) where T : IResourceType } if (!matched && transformableMatches.Count > 0) { - preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform, bundle); + preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform, currentBundle); urls.Length = 0; transformableMatches.Clear(); } } if (transformableMatches.Count > 0) { - preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform, bundle); + preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform, currentBundle); urls.Length = 0; transformableMatches.Clear(); } @@ -203,7 +203,15 @@ public string EmptyDeferredResourceBundle() where T : IResourceType string transform = transformBuilder.ToString(); var noCommentTransform = Regex.HtmlCommentPattern.Replace(transform, string.Empty); transform = DoTransform(transform, urls, transformableMatches, noCommentTransform, bundle); - completeTransform.Append(transform); + + if (resource.IsDynamicLoad(bundle)) + { + completeTransform.Insert(0, transform); + } + else + { + completeTransform.Append(transform); + } } } } diff --git a/RequestReduce/ResourceTypes/CssResource.cs b/RequestReduce/ResourceTypes/CssResource.cs index ccfb0e2..4506e2b 100644 --- a/RequestReduce/ResourceTypes/CssResource.cs +++ b/RequestReduce/ResourceTypes/CssResource.cs @@ -38,7 +38,12 @@ public Regex ResourceRegex public Func TagValidator { get; set; } - public bool IsDeferred(int bundle) + public bool IsLoadDeferred(int bundle) + { + return false; + } + + public bool IsDynamicLoad(int bundle) { return false; } diff --git a/RequestReduce/ResourceTypes/IResourceType.cs b/RequestReduce/ResourceTypes/IResourceType.cs index 78cc8e2..b9e7246 100644 --- a/RequestReduce/ResourceTypes/IResourceType.cs +++ b/RequestReduce/ResourceTypes/IResourceType.cs @@ -9,7 +9,8 @@ public interface IResourceType string FileName { get; } IEnumerable SupportedMimeTypes { get; } int Bundle(string resource); - bool IsDeferred(int bundle); + bool IsLoadDeferred(int bundle); + bool IsDynamicLoad(int bundle); string TransformedMarkupTag(string url, int bundle); Regex ResourceRegex { get; } Func TagValidator { get; set; } diff --git a/RequestReduce/ResourceTypes/JavaScriptResource.cs b/RequestReduce/ResourceTypes/JavaScriptResource.cs index 079f68b..9879cd1 100644 --- a/RequestReduce/ResourceTypes/JavaScriptResource.cs +++ b/RequestReduce/ResourceTypes/JavaScriptResource.cs @@ -23,8 +23,11 @@ private enum ScriptBundle public JavaScriptResource() { ScriptFormats[(int)ScriptBundle.Default] = @""; - ScriptFormats[(int)ScriptBundle.Async] = @""; ScriptFormats[(int)ScriptBundle.Defer] = @""; + ScriptFormats[(int)ScriptBundle.Async] = @""; } private Func tagValidator = ((tag, url) => @@ -87,13 +90,22 @@ public Func TagValidator } } - public bool IsDeferred(int bundle) + public bool IsLoadDeferred(int bundle) { - if(bundle == (int)ScriptBundle.Async || bundle == (int)ScriptBundle.Defer) + if (bundle == (int)ScriptBundle.Defer) + { + return true; + } + if(bundle == (int)ScriptBundle.Async) { return true; } return false; } + + public bool IsDynamicLoad(int bundle) + { + return (bundle == (int)ScriptBundle.Async); + } } } From 3b8a96d0bfe480c89f611843e611a1fab495dc87 Mon Sep 17 00:00:00 2001 From: Mads Storm Date: Sun, 12 Feb 2012 13:23:41 +0100 Subject: [PATCH 4/8] Empty deferred bundles at body-end and html-end --- RequestReduce.Facts/RRContainerFacts.cs | 2 +- RequestReduce/Module/ResponseFilter.cs | 48 ++++++++++---- RequestReduce/Module/ResponseTransformer.cs | 66 +++++++++++-------- RequestReduce/ResourceTypes/CssResource.cs | 2 +- RequestReduce/ResourceTypes/IResourceType.cs | 2 +- .../ResourceTypes/JavaScriptResource.cs | 2 +- RequestReduce/Utilities/RegexCache.cs | 2 + 7 files changed, 78 insertions(+), 46 deletions(-) diff --git a/RequestReduce.Facts/RRContainerFacts.cs b/RequestReduce.Facts/RRContainerFacts.cs index 2154e00..dd0c19f 100644 --- a/RequestReduce.Facts/RRContainerFacts.cs +++ b/RequestReduce.Facts/RRContainerFacts.cs @@ -29,7 +29,7 @@ public System.Collections.Generic.IEnumerable SupportedMimeTypes get { throw new System.NotImplementedException(); } } - public int Bundle(string resource) + public int BundleId(string resource) { throw new System.NotImplementedException(); } diff --git a/RequestReduce/Module/ResponseFilter.cs b/RequestReduce/Module/ResponseFilter.cs index 3a16424..2fc02f3 100644 --- a/RequestReduce/Module/ResponseFilter.cs +++ b/RequestReduce/Module/ResponseFilter.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using RequestReduce.Api; -using System.Web; +using RequestReduce.Utilities; namespace RequestReduce.Module { @@ -12,6 +12,7 @@ public class ResponseFilter : AbstractFilter { private readonly Encoding encoding; private readonly IResponseTransformer responseTransformer; + private static readonly RegexCache Regex = new RegexCache(); private byte[][] startStringUpper; private bool[] currentStartStringsToSkip; private byte[][] startStringLower; @@ -90,7 +91,7 @@ public override bool CanWrite public override void Close() { - WriteDeferredBundles(); + EmptyDeferredBundles(); Closed = true; BaseStream.Close(); } @@ -101,7 +102,7 @@ public override void Flush() { var transformed = encoding.GetBytes(responseTransformer.Transform(encoding.GetString(transformBuffer.ToArray()))); - BaseStream.Write(transformed, 0, transformed.Length); + WriteBaseStream(transformed, 0, transformed.Length); transformBuffer.Clear(); } @@ -129,10 +130,10 @@ public override void Write(byte[] buffer, int offset, int count) if (endTransformPosition > 0) { if ((actualOffset + actualLength) - endTransformPosition > 0) - BaseStream.Write(buffer, endTransformPosition, (actualOffset + actualLength) - endTransformPosition); + WriteBaseStream(buffer, endTransformPosition, (actualOffset + actualLength) - endTransformPosition); } else - BaseStream.Write(buffer, actualOffset, actualLength); + WriteBaseStream(buffer, actualOffset, actualLength); break; case SearchState.MatchingStart: case SearchState.MatchingStartClose: @@ -140,7 +141,7 @@ public override void Write(byte[] buffer, int offset, int count) case SearchState.MatchingStop: case SearchState.LookForAdjacentScript: if (startTransformPosition > actualOffset) - BaseStream.Write(buffer, actualOffset, startTransformPosition - actualOffset); + WriteBaseStream(buffer, actualOffset, startTransformPosition - actualOffset); break; } RRTracer.Trace("Ending Filter Write"); @@ -176,7 +177,7 @@ private int HandleMatchingStartCloseMatch(int i, byte b) return 0; } if (i - originalOffset < transformBuffer.Count) - BaseStream.Write(transformBuffer.ToArray(), 0, (transformBuffer.Count - (i - originalOffset))); + WriteBaseStream(transformBuffer.ToArray(), 0, (transformBuffer.Count - (i - originalOffset))); transformBuffer.Clear(); state = SearchState.LookForStart; return 0; @@ -221,12 +222,12 @@ private void DoTransform(byte[] buffer, ref int startTransformPosition) { currentStartStringsToSkip = new bool[currentStartStringsToSkip.Length]; if ((startTransformPosition - actualOffset) >= 0) - BaseStream.Write(buffer, actualOffset, startTransformPosition - actualOffset); + WriteBaseStream(buffer, actualOffset, startTransformPosition - actualOffset); try { var transformed = encoding.GetBytes(responseTransformer.Transform(encoding.GetString(transformBuffer.ToArray()))); - BaseStream.Write(transformed, 0, transformed.Length); + WriteBaseStream(transformed, 0, transformed.Length); } catch (Exception ex) { @@ -237,7 +238,7 @@ private void DoTransform(byte[] buffer, ref int startTransformPosition) RRTracer.Trace(ex.ToString()); if (Registry.CaptureErrorAction != null) Registry.CaptureErrorAction(wrappedException); - BaseStream.Write(transformBuffer.ToArray(), 0, transformBuffer.Count); + WriteBaseStream(transformBuffer.ToArray(), 0, transformBuffer.Count); } startTransformPosition = 0; transformBuffer.Clear(); @@ -287,7 +288,7 @@ private int HandleMatchingStartMatch(byte b, ref int i, byte[] buffer, ref int e else { if(i-originalOffset < transformBuffer.Count) - BaseStream.Write(transformBuffer.ToArray(), 0, (transformBuffer.Count - (i - originalOffset))); + WriteBaseStream(transformBuffer.ToArray(), 0, (transformBuffer.Count - (i - originalOffset))); transformBuffer.Clear(); } state = SearchState.LookForStart; @@ -419,12 +420,33 @@ public override long Position } } - - private void WriteDeferredBundles() + private void EmptyDeferredBundles() { string deferred = responseTransformer.EmptyDeferredBundles(); byte[] buffer = encoding.GetBytes(deferred); BaseStream.Write(buffer, 0, buffer.Length); } + + private void WriteBaseStream(byte[] inputBuffer, int offset, int count) + { + string stringToWrite = encoding.GetString(inputBuffer, offset, count); + + stringToWrite = TryEmptyDeferredBundles(stringToWrite, Regex.BodyEndPattern); + stringToWrite = TryEmptyDeferredBundles(stringToWrite, Regex.HtmlEndPattern); + + byte[] bufferToWrite = encoding.GetBytes(stringToWrite); + BaseStream.Write(bufferToWrite, 0, bufferToWrite.Length); + } + + private string TryEmptyDeferredBundles(string str, System.Text.RegularExpressions.Regex regex) + { + var match = regex.Match(str); + if (match.Success) + { + str = regex.Replace(str, responseTransformer.EmptyDeferredBundles() + match.ToString(), 1); + } + + return str; + } } } diff --git a/RequestReduce/Module/ResponseTransformer.cs b/RequestReduce/Module/ResponseTransformer.cs index 134b498..1675f2e 100644 --- a/RequestReduce/Module/ResponseTransformer.cs +++ b/RequestReduce/Module/ResponseTransformer.cs @@ -65,13 +65,13 @@ private string Transform(string preTransform) where T : IResourceType var url = RelativeToAbsoluteUtility.ToAbsolute(config.BaseAddress == null ? context.Request.Url : new Uri(config.BaseAddress), urlMatch.Groups["url"].Value); if ((resource.TagValidator == null || resource.TagValidator(strMatch, url)) && (RRContainer.Current.GetAllInstances().Where(x => (x is CssFilter && typeof(T) == typeof(CssResource)) || (x is JavascriptFilter && typeof(T) == typeof(JavaScriptResource))).FirstOrDefault(y => y.IgnoreTarget(new CssJsFilterContext(context.Request, url, strMatch))) == null)) { - int matchBundle = resource.Bundle(strMatch); + int matchBundle = resource.BundleId(strMatch); if(!resource.IsLoadDeferred(matchBundle)) { if ((transformableMatches.Count == 0) || (matchBundle == currentBundle)) { matched = true; - currentBundle = resource.Bundle(strMatch); + currentBundle = resource.BundleId(strMatch); urls.Append(url); urls.Append(GetMedia(strMatch)); urls.Append("::"); @@ -180,41 +180,49 @@ public string EmptyDeferredResourceBundle() where T : IResourceType if (deferredResources.ContainsKey(resource)) { var bundles = deferredResources[resource]; - foreach (int bundle in bundles.Keys) + if (bundles != null) { - if (bundles.ContainsKey(bundle)) + foreach (int bundleId in bundles.Keys) { - var urls = new StringBuilder(); - var transformableMatches = new List(); - StringBuilder transformBuilder = new StringBuilder(); - - var matches = bundles[bundle]; - foreach (var match in matches) + if (bundles[bundleId] != null) { - string url = match.Key; - string strMatch = match.Value; - urls.Append(url); - urls.Append(GetMedia(strMatch)); - urls.Append("::"); - transformableMatches.Add(strMatch); - transformBuilder.Append(strMatch); - } + var urls = new StringBuilder(); + var transformableMatches = new List(); + StringBuilder transformBuilder = new StringBuilder(); - string transform = transformBuilder.ToString(); - var noCommentTransform = Regex.HtmlCommentPattern.Replace(transform, string.Empty); - transform = DoTransform(transform, urls, transformableMatches, noCommentTransform, bundle); + var matches = bundles[bundleId]; + if (matches.Count > 0) + { + foreach (var match in matches) + { + string url = match.Key; + string strMatch = match.Value; + urls.Append(url); + urls.Append(GetMedia(strMatch)); + urls.Append("::"); + transformableMatches.Add(strMatch); + transformBuilder.Append(strMatch); + } + bundles[bundleId].Clear(); - if (resource.IsDynamicLoad(bundle)) - { - completeTransform.Insert(0, transform); - } - else - { - completeTransform.Append(transform); + string transform = transformBuilder.ToString(); + var noCommentTransform = Regex.HtmlCommentPattern.Replace(transform, string.Empty); + transform = DoTransform(transform, urls, transformableMatches, noCommentTransform, bundleId); + + if (resource.IsDynamicLoad(bundleId)) + { + completeTransform.Insert(0, transform); + } + else + { + completeTransform.Append(transform); + } + } } - } + } } } + return completeTransform.ToString(); } diff --git a/RequestReduce/ResourceTypes/CssResource.cs b/RequestReduce/ResourceTypes/CssResource.cs index 4506e2b..0e53861 100644 --- a/RequestReduce/ResourceTypes/CssResource.cs +++ b/RequestReduce/ResourceTypes/CssResource.cs @@ -19,7 +19,7 @@ public IEnumerable SupportedMimeTypes get { return new[] { "text/css" }; } } - public int Bundle(string resource) + public int BundleId(string resource) { return 0; } diff --git a/RequestReduce/ResourceTypes/IResourceType.cs b/RequestReduce/ResourceTypes/IResourceType.cs index b9e7246..6fd2b55 100644 --- a/RequestReduce/ResourceTypes/IResourceType.cs +++ b/RequestReduce/ResourceTypes/IResourceType.cs @@ -8,7 +8,7 @@ public interface IResourceType { string FileName { get; } IEnumerable SupportedMimeTypes { get; } - int Bundle(string resource); + int BundleId(string resource); bool IsLoadDeferred(int bundle); bool IsDynamicLoad(int bundle); string TransformedMarkupTag(string url, int bundle); diff --git a/RequestReduce/ResourceTypes/JavaScriptResource.cs b/RequestReduce/ResourceTypes/JavaScriptResource.cs index 9879cd1..c2bbf78 100644 --- a/RequestReduce/ResourceTypes/JavaScriptResource.cs +++ b/RequestReduce/ResourceTypes/JavaScriptResource.cs @@ -49,7 +49,7 @@ public IEnumerable SupportedMimeTypes get { return new[] { "text/javascript", "application/javascript", "application/x-javascript" }; } } - public int Bundle(string resource) + public int BundleId(string resource) { // Some real Regex needed here !!! // Some real Regex needed here !!! diff --git a/RequestReduce/Utilities/RegexCache.cs b/RequestReduce/Utilities/RegexCache.cs index bef5c8f..6868896 100644 --- a/RequestReduce/Utilities/RegexCache.cs +++ b/RequestReduce/Utilities/RegexCache.cs @@ -18,5 +18,7 @@ internal class RegexCache internal readonly Regex PseudoElementPattern = new Regex(@":before|:after|:first-line|:first-letter", RegexOptions.Compiled); internal readonly Regex HtmlCommentPattern = new Regex(@"", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); internal readonly Regex PrivateIpPattern = new Regex(@"^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|^::1$|^fd[0-9a-f]{2}:.+", RegexOptions.Compiled | RegexOptions.IgnoreCase); + internal readonly Regex BodyEndPattern = new Regex(@"", RegexOptions.Compiled | RegexOptions.IgnoreCase); + internal readonly Regex HtmlEndPattern = new Regex(@"", RegexOptions.Compiled | RegexOptions.IgnoreCase); } } From 016f85c4ebb7d4cf5008cf3d818b2a2ee4392483 Mon Sep 17 00:00:00 2001 From: Mads Storm Date: Sun, 12 Feb 2012 14:18:14 +0100 Subject: [PATCH 5/8] Test defer/async bundling --- .../Module/ResponseFilterFacts.cs | 39 +++++++++++++++++++ .../Module/ResponseTransformerFacts.cs | 33 +--------------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/RequestReduce.Facts/Module/ResponseFilterFacts.cs b/RequestReduce.Facts/Module/ResponseFilterFacts.cs index c6c1e17..aee4593 100644 --- a/RequestReduce.Facts/Module/ResponseFilterFacts.cs +++ b/RequestReduce.Facts/Module/ResponseFilterFacts.cs @@ -5,6 +5,7 @@ using RequestReduce.Api; using RequestReduce.Module; using Xunit; +using System.Web; namespace RequestReduce.Facts.Module { @@ -368,6 +369,44 @@ public void WillTransformAdjacntScritTagsInBodyTagsSeparatedByCommentsAndNoscrip Assert.Equal(@"theadafterend", testable.FilteredResult); } + [Fact] + public void WillBundleAsyncAndDeferScriptsBeforeBodyEnd() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + + + + + +site + "; + var expected = @" + + + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::")).Returns("http://server/Me7.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me3.js::http://server/Me4.js::")).Returns("http://server/Me8.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me5.js::http://server/Me6.js::")).Returns("http://server/Me9.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } } } } \ No newline at end of file diff --git a/RequestReduce.Facts/Module/ResponseTransformerFacts.cs b/RequestReduce.Facts/Module/ResponseTransformerFacts.cs index 4ef3c91..e9dd70f 100644 --- a/RequestReduce.Facts/Module/ResponseTransformerFacts.cs +++ b/RequestReduce.Facts/Module/ResponseTransformerFacts.cs @@ -14,7 +14,7 @@ namespace RequestReduce.Facts.Module { public class ResponseTransformerFacts { - private class TestableResponseTransformer : Testable + internal class TestableResponseTransformer : Testable { public TestableResponseTransformer() { @@ -777,37 +777,6 @@ public void WillTransformHeadWithDuplicateScriptsAndInlineScript() Assert.Equal(transformed, result); RRContainer.Current = null; } - - - [Fact] - public void WillBundleAsyncAndDeferScriptsSeparately() - { - var testable = new TestableResponseTransformer(); - var transform = @" - - - - - - - -site - "; - var transformed = @" - -site - "; - testable.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::")).Returns("http://server/Me7.js"); - testable.Mock().Setup(x => x.FindReduction("http://server/Me3.js::http://server/Me4.js::")).Returns("http://server/Me8.js"); - testable.Mock().Setup(x => x.FindReduction("http://server/Me5.js::http://server/Me6.js::")).Returns("http://server/Me9.js"); - testable.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); - - var result = testable.ClassUnderTest.Transform(transform); - - Assert.Equal(transformed, result); - } - - } } } From aceac39863cb1b56a8a846b8b05b82314e43a692 Mon Sep 17 00:00:00 2001 From: Mads Storm Date: Mon, 13 Feb 2012 19:25:43 +0100 Subject: [PATCH 6/8] Tests for Async/Defer bundle --- .../Module/ResponseFilterFacts.cs | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) diff --git a/RequestReduce.Facts/Module/ResponseFilterFacts.cs b/RequestReduce.Facts/Module/ResponseFilterFacts.cs index aee4593..df2869a 100644 --- a/RequestReduce.Facts/Module/ResponseFilterFacts.cs +++ b/RequestReduce.Facts/Module/ResponseFilterFacts.cs @@ -407,6 +407,280 @@ public void WillBundleAsyncAndDeferScriptsBeforeBodyEnd() Assert.Equal(expected, testableFilter.FilteredResult); } + + [Fact] + public void WillBundleMixedAsyncAndDeferScriptsBeforeBodyEnd() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + + + + + + + +site + "; + var expected = @" + + + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::")).Returns("http://server/Me9.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me3.js::http://server/Me4.js::")).Returns("http://server/Me10.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me5.js::http://server/Me6.js::")).Returns("http://server/Me11.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me7.js::http://server/Me8.js::")).Returns("http://server/Me12.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + + + [Fact] + public void WillIgnoreAsyncInSrcAttribute() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + +site + "; + var expected = @" + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/my%20async%20script.js::http://server/Me2.js::")).Returns("http://server/Me3.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + [Fact] + public void WillIgnoreAsyncInSingleQuoteSrcAttribute() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + +site + "; + var expected = @" + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/my%20async%20script.js::http://server/Me2.js::")).Returns("http://server/Me3.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + [Fact] + public void WillIgnoreInlineScriptWithAsyncAndDeferContents() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + +site + "; + var expected = @" + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + [Fact] + public void WillIgnoreDeferInSrcAttribute() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + +site + "; + var expected = @" + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/my%20defer%20script.js::http://server/Me2.js::")).Returns("http://server/Me3.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + [Fact] + public void WillIgnoreDeferInSingleQuoteSrcAttribute() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + +site + "; + var expected = @" + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/my%20defer%20script.js::http://server/Me2.js::")).Returns("http://server/Me3.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + [Fact] + public void WillBundleVariousDeferScriptsBeforeBodyEnd() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + + + + + + + + +site + "; + var expected = @" + + + + + + + + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::http://server/Me3.js::http://server/Me4.js::http://server/Me5.js::http://server/Me6.js::http://server/Me7.js::http://server/Me8.js::http://server/Me9.js::")).Returns("http://server/Me10.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + + [Fact] + public void WillBundleVariousAsyncScriptsBeforeBodyEnd() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + + + + + + + + +site + "; + var expected = @" + + + + + + + + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::http://server/Me3.js::http://server/Me4.js::http://server/Me5.js::http://server/Me6.js::http://server/Me7.js::http://server/Me8.js::http://server/Me9.js::")).Returns("http://server/Me10.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } } } } \ No newline at end of file From 5483acfb8f2d6b8abc967c41909a9c50492d360d Mon Sep 17 00:00:00 2001 From: Mads Storm Date: Mon, 13 Feb 2012 19:41:05 +0100 Subject: [PATCH 7/8] More async/defer tests --- .../Module/ResponseFilterFacts.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/RequestReduce.Facts/Module/ResponseFilterFacts.cs b/RequestReduce.Facts/Module/ResponseFilterFacts.cs index df2869a..00d8375 100644 --- a/RequestReduce.Facts/Module/ResponseFilterFacts.cs +++ b/RequestReduce.Facts/Module/ResponseFilterFacts.cs @@ -681,6 +681,57 @@ public void WillBundleVariousAsyncScriptsBeforeBodyEnd() Assert.Equal(expected, testableFilter.FilteredResult); } + + [Fact] + public void WillIgnoreExternalScriptWithAsyncAndDeferInlinedContents() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + +site + "; + var expected = @" + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + [Fact] + public void WillIgnoreInlineScriptWithAsyncAndDeferAttributes() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + +site + "; + var expected = @" + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + } } } \ No newline at end of file From 6082a79b1f927c9afbbc4ddac490ff0e3588616d Mon Sep 17 00:00:00 2001 From: Mads Storm Date: Mon, 13 Feb 2012 22:29:15 +0100 Subject: [PATCH 8/8] Async/defer RegEx and more tests --- .../Module/ResponseFilterFacts.cs | 206 +++++++++++++++++- RequestReduce/Module/ResponseTransformer.cs | 3 +- RequestReduce/RequestReduce.csproj | 1 + .../ResourceTypes/JavaScriptResource.cs | 35 ++- 4 files changed, 231 insertions(+), 14 deletions(-) diff --git a/RequestReduce.Facts/Module/ResponseFilterFacts.cs b/RequestReduce.Facts/Module/ResponseFilterFacts.cs index 00d8375..1f02fd4 100644 --- a/RequestReduce.Facts/Module/ResponseFilterFacts.cs +++ b/RequestReduce.Facts/Module/ResponseFilterFacts.cs @@ -449,8 +449,6 @@ public void WillBundleMixedAsyncAndDeferScriptsBeforeBodyEnd() Assert.Equal(expected, testableFilter.FilteredResult); } - - [Fact] public void WillIgnoreAsyncInSrcAttribute() @@ -468,13 +466,14 @@ public void WillIgnoreAsyncInSrcAttribute() site "; var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); testableTransformer.Mock().Setup(x => x.FindReduction("http://server/my%20async%20script.js::http://server/Me2.js::")).Returns("http://server/Me3.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me2.js::")).Returns("http://server/Me4.js"); testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); testableFilter.Inject(testableTransformer.ClassUnderTest); @@ -499,13 +498,14 @@ public void WillIgnoreAsyncInSingleQuoteSrcAttribute() site "; var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); testableTransformer.Mock().Setup(x => x.FindReduction("http://server/my%20async%20script.js::http://server/Me2.js::")).Returns("http://server/Me3.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me2.js::")).Returns("http://server/Me4.js"); testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); testableFilter.Inject(testableTransformer.ClassUnderTest); @@ -732,6 +732,204 @@ public void WillIgnoreInlineScriptWithAsyncAndDeferAttributes() Assert.Equal(expected, testableFilter.FilteredResult); } + [Fact] + public void WillIgnoreDeferInCustomAttribute() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + +site + "; + var expected = @" + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::")).Returns("http://server/Me3.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + [Fact] + public void WillIgnoreDeferInSingleQuoteCustomAttribute() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + +site + "; + var expected = @" + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::")).Returns("http://server/Me3.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + [Fact] + public void WillIgnoreAsyncInCustomAttribute() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + +site + "; + var expected = @" + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::")).Returns("http://server/Me3.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me2.js::")).Returns("http://server/Me4.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + [Fact] + public void WillIgnoreAsyncInSingleQuoteCustomAttribute() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + +site + "; + var expected = @" + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::")).Returns("http://server/Me3.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me2.js::")).Returns("http://server/Me4.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + [Fact] + public void WillBundleAsyncAndDeferScriptsBeforeHtmlEnd() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + + + + + +site + "; + var expected = @" + + + + + +site + "; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::")).Returns("http://server/Me7.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me3.js::http://server/Me4.js::")).Returns("http://server/Me8.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me5.js::http://server/Me6.js::")).Returns("http://server/Me9.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + + Assert.Equal(expected, testableFilter.FilteredResult); + } + + + [Fact] + public void WillBundleAsyncAndDeferScriptsBeforeStreamClose() + { + var testableFilter = new TestableResponseFilter(Encoding.UTF8); + + var testBuffer = @" + + + + + + + +site"; + + var expected = @" + + + + + +site"; + + var testableTransformer = new RequestReduce.Facts.Module.ResponseTransformerFacts.TestableResponseTransformer(); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me.js::http://server/Me2.js::")).Returns("http://server/Me7.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me3.js::http://server/Me4.js::")).Returns("http://server/Me8.js"); + testableTransformer.Mock().Setup(x => x.FindReduction("http://server/Me5.js::http://server/Me6.js::")).Returns("http://server/Me9.js"); + testableTransformer.Mock().Setup(x => x.Request.Url).Returns(new Uri("http://server/megah")); + + testableFilter.Inject(testableTransformer.ClassUnderTest); + testableFilter.ClassUnderTest.Write(Encoding.UTF8.GetBytes(testBuffer), 0, testBuffer.Length); + testableFilter.ClassUnderTest.Close(); + + Assert.Equal(expected, testableFilter.FilteredResult); + } } } } \ No newline at end of file diff --git a/RequestReduce/Module/ResponseTransformer.cs b/RequestReduce/Module/ResponseTransformer.cs index 1675f2e..687e3d1 100644 --- a/RequestReduce/Module/ResponseTransformer.cs +++ b/RequestReduce/Module/ResponseTransformer.cs @@ -206,8 +206,7 @@ public string EmptyDeferredResourceBundle() where T : IResourceType bundles[bundleId].Clear(); string transform = transformBuilder.ToString(); - var noCommentTransform = Regex.HtmlCommentPattern.Replace(transform, string.Empty); - transform = DoTransform(transform, urls, transformableMatches, noCommentTransform, bundleId); + transform = DoTransform(transform, urls, transformableMatches, transform, bundleId); if (resource.IsDynamicLoad(bundleId)) { diff --git a/RequestReduce/RequestReduce.csproj b/RequestReduce/RequestReduce.csproj index 06cafc0..6b13ca7 100644 --- a/RequestReduce/RequestReduce.csproj +++ b/RequestReduce/RequestReduce.csproj @@ -78,6 +78,7 @@ + diff --git a/RequestReduce/ResourceTypes/JavaScriptResource.cs b/RequestReduce/ResourceTypes/JavaScriptResource.cs index 947ec96..b01d9c9 100644 --- a/RequestReduce/ResourceTypes/JavaScriptResource.cs +++ b/RequestReduce/ResourceTypes/JavaScriptResource.cs @@ -19,6 +19,7 @@ private enum ScriptBundle private readonly string[] ScriptFormats = new string[Enum.GetValues(typeof(ScriptBundle)).Length]; private readonly Regex scriptPattern = new Regex(@"|)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); private static readonly Regex ScriptFilterPattern = new Regex(@"^]+src=(.*?)(/>|>(\s*?))", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); + private static readonly Regex TagPattern = new System.Web.RegularExpressions.TagRegex(); public JavaScriptResource() { @@ -51,17 +52,35 @@ public IEnumerable SupportedMimeTypes public int BundleId(string resource) { - // Some real Regex needed here !!! - // Some real Regex needed here !!! + var tagMatch = TagPattern.Match(resource); - if (resource.Contains("async")) + if (tagMatch.Success) { - return (int)ScriptBundle.Async; - } + if (TagPattern.GetGroupNames().Contains("attrname")) + { + Group attrNames = tagMatch.Groups["attrname"]; + if (attrNames != null) + { + if (attrNames.Captures != null) + { + foreach (Capture attr in attrNames.Captures) + { + if (String.Compare(attr.Value, "async", StringComparison.InvariantCultureIgnoreCase) == 0) + { + return (int)ScriptBundle.Async; + } + } - if (resource.Contains("defer")) - { - return (int)ScriptBundle.Defer; + foreach (Capture attr in attrNames.Captures) + { + if (String.Compare(attr.Value, "defer", StringComparison.InvariantCultureIgnoreCase) == 0) + { + return (int)ScriptBundle.Defer; + } + } + } + } + } } return (int)ScriptBundle.Default;