parent
9b69e6f234
commit
e282d2a861
|
|
@ -20,11 +20,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||||
{
|
{
|
||||||
private static readonly char[] PatternSeparator = new[] { ',' };
|
private static readonly char[] PatternSeparator = new[] { ',' };
|
||||||
|
|
||||||
|
private static readonly PathComparer DefaultPathComparer = new PathComparer();
|
||||||
|
|
||||||
private readonly FileProviderGlobbingDirectory _baseGlobbingDirectory;
|
private readonly FileProviderGlobbingDirectory _baseGlobbingDirectory;
|
||||||
|
|
||||||
// Internal for testing
|
// Internal for testing
|
||||||
internal GlobbingUrlBuilder() { }
|
internal GlobbingUrlBuilder() { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="GlobbingUrlBuilder"/>.
|
/// Creates a new <see cref="GlobbingUrlBuilder"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -95,7 +97,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||||
{
|
{
|
||||||
return Enumerable.Empty<string>();
|
return Enumerable.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Cache != null)
|
if (Cache != null)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{nameof(GlobbingUrlBuilder)}-inc:{include}-exc:{exclude}";
|
var cacheKey = $"{nameof(GlobbingUrlBuilder)}-inc:{include}-exc:{exclude}";
|
||||||
|
|
@ -127,7 +129,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||||
|
|
||||||
var matches = matcher.Execute(_baseGlobbingDirectory);
|
var matches = matcher.Execute(_baseGlobbingDirectory);
|
||||||
|
|
||||||
return matches.Files.Select(ResolveMatchedPath);
|
return matches.Files.Select(ResolveMatchedPath)
|
||||||
|
.OrderBy(path => path, DefaultPathComparer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResolveMatchedPath(string matchedPath)
|
private string ResolveMatchedPath(string matchedPath)
|
||||||
|
|
@ -137,6 +140,72 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||||
return RequestPathBase.Add(relativePath).ToString();
|
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)
|
private static string TrimLeadingSlash(string value)
|
||||||
{
|
{
|
||||||
var result = value;
|
var result = value;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNet.FileProviders;
|
using Microsoft.AspNet.FileProviders;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
|
|
@ -51,6 +52,179 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||||
url => Assert.Equal("/blank.css", url));
|
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]
|
[Theory]
|
||||||
[InlineData("/sub")]
|
[InlineData("/sub")]
|
||||||
[InlineData("/sub/again")]
|
[InlineData("/sub/again")]
|
||||||
|
|
@ -70,8 +244,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(urlList,
|
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]
|
[Fact]
|
||||||
|
|
@ -79,7 +253,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var fileProvider = MakeFileProvider();
|
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 requestPathBase = PathString.Empty;
|
||||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||||
|
|
||||||
|
|
@ -91,8 +265,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(urlList,
|
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]
|
[Fact]
|
||||||
|
|
@ -128,8 +302,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(urlList,
|
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();
|
cacheSetContext.VerifyAll();
|
||||||
Mock.Get(cache).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));
|
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>();
|
var fileInfo = new Mock<IFileInfo>();
|
||||||
fileInfo.Setup(f => f.Name).Returns(name);
|
fileInfo.Setup(f => f.Name).Returns(name);
|
||||||
|
fileInfo.Setup(f => f.IsDirectory).Returns(isDirectory);
|
||||||
return fileInfo.Object;
|
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)
|
private static IDirectoryContents MakeDirectoryContents(params string[] fileNames)
|
||||||
{
|
{
|
||||||
var files = fileNames.Select(name => MakeFileInfo(name));
|
var files = fileNames.Select(name => MakeFileInfo(name));
|
||||||
|
|
@ -215,7 +447,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||||
.Returns(directoryContents);
|
.Returns(directoryContents);
|
||||||
return fileProvider.Object;
|
return fileProvider.Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IMemoryCache MakeCache(object result = null)
|
private static IMemoryCache MakeCache(object result = null)
|
||||||
{
|
{
|
||||||
var cache = new Mock<IMemoryCache>();
|
var cache = new Mock<IMemoryCache>();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue