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(@"^)", 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);
}
}