diff --git a/RequestReduce.Facts/Module/ResponseFilterFacts.cs b/RequestReduce.Facts/Module/ResponseFilterFacts.cs index c6c1e17..1f02fd4 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,567 @@ 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); + } + + [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.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 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.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 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); + } + + [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); + } + + [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.Facts/Module/ResponseTransformerFacts.cs b/RequestReduce.Facts/Module/ResponseTransformerFacts.cs index e61f3e8..8038387 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() { @@ -791,7 +791,6 @@ public void WillTransformHeadWithDuplicateScriptsAndInlineScript() Assert.Equal(transformed, result); RRContainer.Current = null; } - } } } diff --git a/RequestReduce.Facts/RRContainerFacts.cs b/RequestReduce.Facts/RRContainerFacts.cs index bef0856..dd0c19f 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 BundleId(string resource) + { + throw new System.NotImplementedException(); + } + + public string TransformedMarkupTag(string url, int bundle) { throw new System.NotImplementedException(); } @@ -41,6 +46,18 @@ public System.Text.RegularExpressions.Regex ResourceRegex public System.Func TagValidator { get; set; } + + + public bool IsLoadDeferred(int bundle) + { + throw new NotImplementedException(); + } + + + public bool IsDynamicLoad(int bundle) + { + throw new NotImplementedException(); + } } [Fact] diff --git a/RequestReduce/Module/ResponseFilter.cs b/RequestReduce/Module/ResponseFilter.cs index cdcd9e0..5523cc1 100644 --- a/RequestReduce/Module/ResponseFilter.cs +++ b/RequestReduce/Module/ResponseFilter.cs @@ -7,6 +7,7 @@ using System.Text; using System.Web; using RequestReduce.Api; +using RequestReduce.Utilities; namespace RequestReduce.Module { @@ -15,6 +16,7 @@ public class ResponseFilter : AbstractFilter private readonly HttpContextBase context; private readonly Encoding encoding; private readonly IResponseTransformer responseTransformer; + private static readonly RegexCache Regex = new RegexCache(); private byte[][] startStringUpper; private bool[] currentStartStringsToSkip; private byte[][] startStringLower; @@ -95,6 +97,7 @@ public override bool CanWrite public override void Close() { + EmptyDeferredBundles(); Closed = true; BaseStream.Close(); } @@ -106,9 +109,10 @@ 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(); } + BaseStream.Flush(); RRTracer.Trace("Flushing Filter"); var filterQs = context != null && context.Request != null ? context.Request.QueryString["rrfilter"] : null; @@ -138,10 +142,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: @@ -149,7 +153,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"); @@ -186,7 +190,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; @@ -231,12 +235,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) { @@ -247,7 +251,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(); @@ -297,7 +301,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; @@ -428,5 +432,34 @@ public override long Position throw new NotSupportedException(); } } + + 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 63b932b..687e3d1 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) { @@ -51,8 +53,10 @@ private string Transform(string preTransform) where T : IResourceType { var urls = new StringBuilder(); var transformableMatches = new List(); - foreach (var match in matches) + int currentBundle = 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 +65,54 @@ 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); + int matchBundle = resource.BundleId(strMatch); + if(!resource.IsLoadDeferred(matchBundle)) + { + if ((transformableMatches.Count == 0) || (matchBundle == currentBundle)) + { + matched = true; + currentBundle = resource.BundleId(strMatch); + urls.Append(url); + urls.Append(GetMedia(strMatch)); + urls.Append("::"); + transformableMatches.Add(strMatch); + } + else + { + cursor--; // This resource into next bundle + } + } + else + { + // This resource into deferred bundle + + var idx = preTransform.IndexOf(strMatch, StringComparison.Ordinal); + preTransform = preTransform.Remove(idx, strMatch.Length); + + 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)); + } } } if (!matched && transformableMatches.Count > 0) { - preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform); + preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform, currentBundle); urls.Length = 0; transformableMatches.Clear(); } } if (transformableMatches.Count > 0) { - preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform); + preTransform = DoTransform(preTransform, urls, transformableMatches, noCommentTransform, currentBundle); urls.Length = 0; transformableMatches.Clear(); } @@ -93,7 +128,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 +145,7 @@ private string DoTransform(string preTransform, StringBuilder urls, List(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]; + if (bundles != null) + { + foreach (int bundleId in bundles.Keys) + { + if (bundles[bundleId] != null) + { + var urls = new StringBuilder(); + var transformableMatches = new List(); + StringBuilder transformBuilder = new StringBuilder(); + + 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(); + + string transform = transformBuilder.ToString(); + transform = DoTransform(transform, urls, transformableMatches, transform, bundleId); + + if (resource.IsDynamicLoad(bundleId)) + { + completeTransform.Insert(0, transform); + } + else + { + completeTransform.Append(transform); + } + } + } + } + } + } + + return completeTransform.ToString(); + } + } } 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/CssResource.cs b/RequestReduce/ResourceTypes/CssResource.cs index f14d4a3..0e53861 100644 --- a/RequestReduce/ResourceTypes/CssResource.cs +++ b/RequestReduce/ResourceTypes/CssResource.cs @@ -19,7 +19,12 @@ public IEnumerable SupportedMimeTypes get { return new[] { "text/css" }; } } - public string TransformedMarkupTag(string url) + public int BundleId(string resource) + { + return 0; + } + + public string TransformedMarkupTag(string url, int bundle) { return string.Format(CssFormat, url); } @@ -31,5 +36,16 @@ public Regex ResourceRegex public Func TagValidator { get; set; } + + + 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 4e7de27..6fd2b55 100644 --- a/RequestReduce/ResourceTypes/IResourceType.cs +++ b/RequestReduce/ResourceTypes/IResourceType.cs @@ -8,7 +8,10 @@ public interface IResourceType { string FileName { get; } IEnumerable SupportedMimeTypes { get; } - string TransformedMarkupTag(string url); + int BundleId(string resource); + 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 36c326e..b01d9c9 100644 --- a/RequestReduce/ResourceTypes/JavaScriptResource.cs +++ b/RequestReduce/ResourceTypes/JavaScriptResource.cs @@ -9,9 +9,28 @@ namespace RequestReduce.ResourceTypes { public class JavaScriptResource : IResourceType { - private const string ScriptFormat = @""; + private enum ScriptBundle + { + Default = 0, + Async = 1, + Defer = 2 + } + + 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() + { + ScriptFormats[(int)ScriptBundle.Default] = @""; + ScriptFormats[(int)ScriptBundle.Defer] = @""; + ScriptFormats[(int)ScriptBundle.Async] = @""; + } + private Func tagValidator = ((tag, url) => { var match = ScriptFilterPattern.Match(tag); @@ -31,9 +50,45 @@ public IEnumerable SupportedMimeTypes get { return new[] { "text/javascript", "application/javascript", "application/x-javascript" }; } } - public string TransformedMarkupTag(string url) + public int BundleId(string resource) + { + var tagMatch = TagPattern.Match(resource); + + if (tagMatch.Success) + { + 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; + } + } + + foreach (Capture attr in attrNames.Captures) + { + if (String.Compare(attr.Value, "defer", StringComparison.InvariantCultureIgnoreCase) == 0) + { + 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 @@ -53,5 +108,23 @@ public Func TagValidator tagValidator = value; } } + + public bool IsLoadDeferred(int bundle) + { + 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); + } } } 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); } }