parent
9b69e6f234
commit
e282d2a861
|
|
@ -20,11 +20,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
{
|
||||
private static readonly char[] PatternSeparator = new[] { ',' };
|
||||
|
||||
private static readonly PathComparer DefaultPathComparer = new PathComparer();
|
||||
|
||||
private readonly FileProviderGlobbingDirectory _baseGlobbingDirectory;
|
||||
|
||||
// Internal for testing
|
||||
internal GlobbingUrlBuilder() { }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="GlobbingUrlBuilder"/>.
|
||||
/// </summary>
|
||||
|
|
@ -95,7 +97,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
{
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
|
||||
if (Cache != null)
|
||||
{
|
||||
var cacheKey = $"{nameof(GlobbingUrlBuilder)}-inc:{include}-exc:{exclude}";
|
||||
|
|
@ -127,7 +129,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
|
||||
var matches = matcher.Execute(_baseGlobbingDirectory);
|
||||
|
||||
return matches.Files.Select(ResolveMatchedPath);
|
||||
return matches.Files.Select(ResolveMatchedPath)
|
||||
.OrderBy(path => path, DefaultPathComparer);
|
||||
}
|
||||
|
||||
private string ResolveMatchedPath(string matchedPath)
|
||||
|
|
@ -137,6 +140,72 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
return RequestPathBase.Add(relativePath).ToString();
|
||||
}
|
||||
|
||||
private class PathComparer : IComparer<string>
|
||||
{
|
||||
public int Compare(string x, string y)
|
||||
{
|
||||
// < 0 = x < y
|
||||
// > 0 = x > y
|
||||
|
||||
if (string.Equals(x, y, StringComparison.Ordinal))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(x) || string.IsNullOrEmpty(y))
|
||||
{
|
||||
return string.Compare(x, y, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
var xExtIndex = x.LastIndexOf('.');
|
||||
var yExtIndex = y.LastIndexOf('.');
|
||||
|
||||
// Ensure extension index is in the last segment, i.e. in the file name
|
||||
var xSlashIndex = x.LastIndexOf('/');
|
||||
var ySlashIndex = y.LastIndexOf('/');
|
||||
xExtIndex = xExtIndex > xSlashIndex ? xExtIndex : -1;
|
||||
yExtIndex = yExtIndex > ySlashIndex ? yExtIndex : -1;
|
||||
|
||||
// Get paths without their extensions, if they have one
|
||||
var xNoExt = xExtIndex >= 0 ? x.Substring(0, xExtIndex) : x;
|
||||
var yNoExt = yExtIndex >= 0 ? y.Substring(0, yExtIndex) : y;
|
||||
|
||||
if (string.Equals(xNoExt, yNoExt, StringComparison.Ordinal))
|
||||
{
|
||||
// Only extension differs so just compare the extension
|
||||
var xExt = xExtIndex >= 0 ? x.Substring(xExtIndex) : string.Empty;
|
||||
var yExt = yExtIndex >= 0 ? y.Substring(yExtIndex) : string.Empty;
|
||||
|
||||
return string.Compare(xExt, yExt, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
var xSegments = xNoExt.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var ySegments = yNoExt.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (xSegments.Length != ySegments.Length)
|
||||
{
|
||||
// Different path depths so shallower path wins
|
||||
return xSegments.Length.CompareTo(ySegments.Length);
|
||||
}
|
||||
|
||||
// Depth is the same so compare each segment
|
||||
for (int i = 0; i < xSegments.Length; i++)
|
||||
{
|
||||
var xSegment = xSegments[i];
|
||||
var ySegment = ySegments[i];
|
||||
|
||||
var xToY = string.Compare(xSegment, ySegment, StringComparison.Ordinal);
|
||||
if (xToY != 0)
|
||||
{
|
||||
return xToY;
|
||||
}
|
||||
}
|
||||
|
||||
// Should't get here, but if we do, hey, they're the same :)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static string TrimLeadingSlash(string value)
|
||||
{
|
||||
var result = value;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
|
@ -51,6 +52,179 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
url => Assert.Equal("/blank.css", url));
|
||||
}
|
||||
|
||||
public static TheoryData OrdersGlobbedMatchResultsCorrectly_Data
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string, FileNode, string[]>
|
||||
{
|
||||
{
|
||||
/* staticUrl */ "/site.css",
|
||||
/* dirStructure */ new FileNode(null, new [] {
|
||||
new FileNode("B", new [] {
|
||||
new FileNode("a.css"),
|
||||
new FileNode("b.css"),
|
||||
new FileNode("ba.css"),
|
||||
new FileNode("b", new [] {
|
||||
new FileNode("a.css")
|
||||
})
|
||||
}),
|
||||
new FileNode("A", new [] {
|
||||
new FileNode("c.css"),
|
||||
new FileNode("d.css")
|
||||
}),
|
||||
new FileNode("a.css")
|
||||
}),
|
||||
/* expectedPaths */ new []
|
||||
{
|
||||
"/site.css",
|
||||
"/a.css",
|
||||
"/A/c.css", "/A/d.css",
|
||||
"/B/a.css", "/B/b.css", "/B/ba.css",
|
||||
"/B/b/a.css"
|
||||
}
|
||||
},
|
||||
{
|
||||
/* staticUrl */ "/site.css",
|
||||
/* dirStructure */ new FileNode(null, new [] {
|
||||
|
||||
new FileNode("A", new [] {
|
||||
new FileNode("c.css"),
|
||||
new FileNode("d.css")
|
||||
}),
|
||||
new FileNode("_A", new [] {
|
||||
new FileNode("1.css"),
|
||||
new FileNode("2.css")
|
||||
}),
|
||||
new FileNode("__A", new [] {
|
||||
new FileNode("1.css"),
|
||||
new FileNode("_1.css")
|
||||
})
|
||||
}),
|
||||
/* expectedPaths */ new []
|
||||
{
|
||||
"/site.css",
|
||||
"/A/c.css", "/A/d.css",
|
||||
"/_A/1.css", "/_A/2.css",
|
||||
"/__A/1.css", "/__A/_1.css"
|
||||
}
|
||||
},
|
||||
{
|
||||
/* staticUrl */ "/site.css",
|
||||
/* dirStructure */ new FileNode(null, new [] {
|
||||
new FileNode("A", new [] {
|
||||
new FileNode("a.b.css"),
|
||||
new FileNode("a-b.css"),
|
||||
new FileNode("a_b.css"),
|
||||
new FileNode("a.css"),
|
||||
new FileNode("a.c.css")
|
||||
})
|
||||
}),
|
||||
/* expectedPaths */ new []
|
||||
{
|
||||
"/site.css",
|
||||
"/A/a.css", "/A/a-b.css", "/A/a.b.css", "/A/a.c.css", "/A/a_b.css"
|
||||
}
|
||||
},
|
||||
{
|
||||
/* staticUrl */ "/site.css",
|
||||
/* dirStructure */ new FileNode(null, new [] {
|
||||
new FileNode("B", new [] {
|
||||
new FileNode("a.bss"),
|
||||
new FileNode("a.css")
|
||||
}),
|
||||
new FileNode("A", new [] {
|
||||
new FileNode("a.css"),
|
||||
new FileNode("a.bss")
|
||||
})
|
||||
}),
|
||||
/* expectedPaths */ new []
|
||||
{
|
||||
"/site.css",
|
||||
"/A/a.bss", "/A/a.css",
|
||||
"/B/a.bss", "/B/a.css"
|
||||
}
|
||||
},
|
||||
{
|
||||
/* staticUrl */ "/site.css",
|
||||
/* dirStructure */ new FileNode(null, new [] {
|
||||
new FileNode("B", new [] {
|
||||
new FileNode("site2.css"),
|
||||
new FileNode("site11.css")
|
||||
}),
|
||||
new FileNode("A", new [] {
|
||||
new FileNode("site2.css"),
|
||||
new FileNode("site11.css")
|
||||
})
|
||||
}),
|
||||
/* expectedPaths */ new []
|
||||
{
|
||||
"/site.css",
|
||||
"/A/site11.css", "/A/site2.css",
|
||||
"/B/site11.css", "/B/site2.css"
|
||||
}
|
||||
},
|
||||
{
|
||||
/* staticUrl */ "/site.css",
|
||||
/* dirStructure */ new FileNode(null, new [] {
|
||||
new FileNode("B", new [] {
|
||||
new FileNode("site"),
|
||||
new FileNode("site.css")
|
||||
}),
|
||||
new FileNode("A", new [] {
|
||||
new FileNode("site.css"),
|
||||
new FileNode("site")
|
||||
})
|
||||
}),
|
||||
/* expectedPaths */ new []
|
||||
{
|
||||
"/site.css",
|
||||
"/A/site", "/A/site.css",
|
||||
"/B/site", "/B/site.css"
|
||||
}
|
||||
},
|
||||
{
|
||||
/* staticUrl */ "/site.css",
|
||||
/* dirStructure */ new FileNode(null, new [] {
|
||||
new FileNode("B.B", new [] {
|
||||
new FileNode("site"),
|
||||
new FileNode("site.css")
|
||||
}),
|
||||
new FileNode("A.A", new [] {
|
||||
new FileNode("site.css"),
|
||||
new FileNode("site")
|
||||
})
|
||||
}),
|
||||
/* expectedPaths */ new []
|
||||
{
|
||||
"/site.css",
|
||||
"/A.A/site", "/A.A/site.css",
|
||||
"/B.B/site", "/B.B/site.css"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(OrdersGlobbedMatchResultsCorrectly_Data))]
|
||||
public void OrdersGlobbedMatchResultsCorrectly(string staticUrl, FileNode dirStructure, string[] expectedPaths)
|
||||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider(dirStructure);
|
||||
IMemoryCache cache = null;
|
||||
var requestPathBase = PathString.Empty;
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
// Act
|
||||
var urlList = globbingUrlBuilder.BuildUrlList(staticUrl, "**/*.*", excludePattern: null);
|
||||
|
||||
// Assert
|
||||
var collectionAssertions = expectedPaths.Select<string, Action<string>>(expected =>
|
||||
actual => Assert.Equal(expected, actual));
|
||||
Assert.Collection(urlList, collectionAssertions.ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/sub")]
|
||||
[InlineData("/sub/again")]
|
||||
|
|
@ -70,8 +244,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
|
||||
// Assert
|
||||
Assert.Collection(urlList,
|
||||
url => Assert.Equal($"{pathBase}/site.css", url),
|
||||
url => Assert.Equal($"{pathBase}/blank.css", url));
|
||||
url => Assert.Equal($"{pathBase}/blank.css", url),
|
||||
url => Assert.Equal($"{pathBase}/site.css", url));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -79,7 +253,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider();
|
||||
var cache = MakeCache(new List<string> { "/site.css", "/blank.css" });
|
||||
var cache = MakeCache(new List<string> { "/blank.css", "/site.css" });
|
||||
var requestPathBase = PathString.Empty;
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
|
|
@ -91,8 +265,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
|
||||
// Assert
|
||||
Assert.Collection(urlList,
|
||||
url => Assert.Equal("/site.css", url),
|
||||
url => Assert.Equal("/blank.css", url));
|
||||
url => Assert.Equal("/blank.css", url),
|
||||
url => Assert.Equal("/site.css", url));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -128,8 +302,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
|
||||
// Assert
|
||||
Assert.Collection(urlList,
|
||||
url => Assert.Equal("/site.css", url),
|
||||
url => Assert.Equal("/blank.css", url));
|
||||
url => Assert.Equal("/blank.css", url),
|
||||
url => Assert.Equal("/site.css", url));
|
||||
cacheSetContext.VerifyAll();
|
||||
Mock.Get(cache).VerifyAll();
|
||||
}
|
||||
|
|
@ -187,13 +361,71 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
Assert.Collection(excludePatterns, pattern => Assert.Equal($"{leadingSlash}**/*.min.css", pattern));
|
||||
}
|
||||
|
||||
private static IFileInfo MakeFileInfo(string name)
|
||||
public class FileNode
|
||||
{
|
||||
public FileNode(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public FileNode(string name, IList<FileNode> children)
|
||||
{
|
||||
Name = name;
|
||||
Children = children;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public IList<FileNode> Children { get; }
|
||||
|
||||
public bool IsDirectory => Children != null && Children.Any();
|
||||
}
|
||||
|
||||
private static IFileInfo MakeFileInfo(string name, bool isDirectory = false)
|
||||
{
|
||||
var fileInfo = new Mock<IFileInfo>();
|
||||
fileInfo.Setup(f => f.Name).Returns(name);
|
||||
fileInfo.Setup(f => f.IsDirectory).Returns(isDirectory);
|
||||
return fileInfo.Object;
|
||||
}
|
||||
|
||||
private static IFileProvider MakeFileProvider(FileNode rootNode)
|
||||
{
|
||||
if (rootNode.Children == null || !rootNode.Children.Any())
|
||||
{
|
||||
throw new ArgumentException(nameof(rootNode));
|
||||
}
|
||||
|
||||
var fileProvider = new Mock<IFileProvider>(MockBehavior.Strict);
|
||||
fileProvider.Setup(fp => fp.GetDirectoryContents(string.Empty))
|
||||
.Returns(MakeDirectoryContents(rootNode, fileProvider));
|
||||
|
||||
return fileProvider.Object;
|
||||
}
|
||||
|
||||
private static IDirectoryContents MakeDirectoryContents(FileNode fileNode, Mock<IFileProvider> fileProviderMock)
|
||||
{
|
||||
var children = new List<IFileInfo>();
|
||||
|
||||
foreach (var node in fileNode.Children)
|
||||
{
|
||||
children.Add(MakeFileInfo(node.Name, node.IsDirectory));
|
||||
if (node.IsDirectory)
|
||||
{
|
||||
var subPath = fileNode.Name != null
|
||||
? (fileNode.Name + "/" + node.Name)
|
||||
: node.Name;
|
||||
fileProviderMock.Setup(fp => fp.GetDirectoryContents(subPath))
|
||||
.Returns(MakeDirectoryContents(node, fileProviderMock));
|
||||
}
|
||||
}
|
||||
|
||||
var directoryContents = new Mock<IDirectoryContents>();
|
||||
directoryContents.Setup(dc => dc.GetEnumerator()).Returns(children.GetEnumerator());
|
||||
|
||||
return directoryContents.Object;
|
||||
}
|
||||
|
||||
private static IDirectoryContents MakeDirectoryContents(params string[] fileNames)
|
||||
{
|
||||
var files = fileNames.Select(name => MakeFileInfo(name));
|
||||
|
|
@ -215,7 +447,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
.Returns(directoryContents);
|
||||
return fileProvider.Object;
|
||||
}
|
||||
|
||||
|
||||
private static IMemoryCache MakeCache(object result = null)
|
||||
{
|
||||
var cache = new Mock<IMemoryCache>();
|
||||
|
|
|
|||
Loading…
Reference in New Issue