Reduce allocations in GlobbingUrlBuilder
This commit is contained in:
parent
e078259547
commit
200fb23ba5
|
|
@ -3,11 +3,12 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.FileSystemGlobbing;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
|
|
@ -17,18 +18,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
/// </summary>
|
||||
public class GlobbingUrlBuilder
|
||||
{
|
||||
private static readonly char[] PatternSeparator = new[] { ',' };
|
||||
private static readonly IReadOnlyList<string> EmptyList =
|
||||
#if NET451
|
||||
new string[0];
|
||||
#else
|
||||
Array.Empty<string>();
|
||||
#endif
|
||||
|
||||
// Valid whitespace characters defined by the HTML5 spec.
|
||||
private static readonly char[] ValidAttributeWhitespaceChars =
|
||||
new[] { '\t', '\n', '\u000C', '\r', ' ' };
|
||||
|
||||
private static readonly PathComparer DefaultPathComparer = new PathComparer();
|
||||
|
||||
private readonly FileProviderGlobbingDirectory _baseGlobbingDirectory;
|
||||
|
||||
public GlobbingUrlBuilder() { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="GlobbingUrlBuilder"/>.
|
||||
/// </summary>
|
||||
|
|
@ -42,6 +44,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
throw new ArgumentNullException(nameof(fileProvider));
|
||||
}
|
||||
|
||||
if (cache == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(cache));
|
||||
}
|
||||
|
||||
FileProvider = fileProvider;
|
||||
Cache = cache;
|
||||
RequestPathBase = requestPathBase;
|
||||
|
|
@ -73,93 +80,103 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
/// <param name="includePattern">The file globbing include pattern.</param>
|
||||
/// <param name="excludePattern">The file globbing exclude pattern.</param>
|
||||
/// <returns>The list of URLs</returns>
|
||||
public virtual ICollection<string> BuildUrlList(string staticUrl, string includePattern, string excludePattern)
|
||||
public virtual IReadOnlyList<string> BuildUrlList(
|
||||
string staticUrl,
|
||||
string includePattern,
|
||||
string excludePattern)
|
||||
{
|
||||
var urls = new HashSet<string>(StringComparer.Ordinal);
|
||||
// Get urls that match the globbing patterns specified
|
||||
var globbedUrls = ExpandGlobbedUrl(includePattern, excludePattern);
|
||||
|
||||
// Add the statically declared url if present
|
||||
if (staticUrl != null)
|
||||
if (staticUrl == null)
|
||||
{
|
||||
urls.Add(staticUrl);
|
||||
return globbedUrls;
|
||||
}
|
||||
|
||||
// Add urls that match the globbing patterns specified
|
||||
var matchedUrls = ExpandGlobbedUrl(includePattern, excludePattern);
|
||||
urls.UnionWith(matchedUrls);
|
||||
// The staticUrl always appears first in the sequence.
|
||||
var urls = new List<string>(1 + globbedUrls.Count)
|
||||
{
|
||||
staticUrl
|
||||
};
|
||||
|
||||
for (var i = 0; i < globbedUrls.Count; i++)
|
||||
{
|
||||
if (!string.Equals(staticUrl, globbedUrls[i], StringComparison.Ordinal))
|
||||
{
|
||||
urls.Add(globbedUrls[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
private IEnumerable<string> ExpandGlobbedUrl(string include, string exclude)
|
||||
private IReadOnlyList<string> ExpandGlobbedUrl(string include, string exclude)
|
||||
{
|
||||
if (string.IsNullOrEmpty(include))
|
||||
{
|
||||
return Enumerable.Empty<string>();
|
||||
return EmptyList;
|
||||
}
|
||||
|
||||
var includePatterns = include.Split(PatternSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
var excludePatterns = exclude?.Split(PatternSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (includePatterns.Length == 0)
|
||||
var cacheKey = new GlobbingUrlKey(include, exclude);
|
||||
List<string> files;
|
||||
if (Cache.TryGetValue(cacheKey, out files))
|
||||
{
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
if (Cache != null)
|
||||
{
|
||||
var cacheKey = $"{nameof(GlobbingUrlBuilder)}-inc:{include}-exc:{exclude}";
|
||||
IEnumerable<string> files;
|
||||
if (!Cache.TryGetValue(cacheKey, out files))
|
||||
{
|
||||
var options = new MemoryCacheEntryOptions();
|
||||
|
||||
for (var i = 0; i < includePatterns.Length; i++)
|
||||
{
|
||||
var changeToken = FileProvider.Watch(includePatterns[i]);
|
||||
options.AddExpirationToken(changeToken);
|
||||
}
|
||||
|
||||
files = FindFiles(includePatterns, excludePatterns);
|
||||
|
||||
Cache.Set(cacheKey, files, options);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
return FindFiles(includePatterns, excludePatterns);
|
||||
}
|
||||
|
||||
private IEnumerable<string> FindFiles(string[] includePatterns, string[] excludePatterns)
|
||||
{
|
||||
var matcher = MatcherBuilder != null ? MatcherBuilder() : new Matcher();
|
||||
var trimmedIncludePatterns = new List<string>();
|
||||
for (var i = 0; i < includePatterns.Length; i++)
|
||||
var includeTokenizer = new StringTokenizer(include, ',');
|
||||
var includeEnumerator = includeTokenizer.GetEnumerator();
|
||||
if (!includeEnumerator.MoveNext())
|
||||
{
|
||||
trimmedIncludePatterns.Add(TrimLeadingTildeSlash(includePatterns[i]));
|
||||
return EmptyList;
|
||||
}
|
||||
|
||||
var options = new MemoryCacheEntryOptions();
|
||||
var trimmedIncludePatterns = new List<string>();
|
||||
foreach (var includePattern in includeTokenizer)
|
||||
{
|
||||
var changeToken = FileProvider.Watch(includePattern.Value);
|
||||
options.AddExpirationToken(changeToken);
|
||||
trimmedIncludePatterns.Add(NormalizePath(includePattern));
|
||||
}
|
||||
var matcher = MatcherBuilder != null ? MatcherBuilder() : new Matcher();
|
||||
matcher.AddIncludePatterns(trimmedIncludePatterns);
|
||||
|
||||
if (excludePatterns != null)
|
||||
if (!string.IsNullOrWhiteSpace(exclude))
|
||||
{
|
||||
var excludeTokenizer = new StringTokenizer(exclude, ',');
|
||||
var trimmedExcludePatterns = new List<string>();
|
||||
for (var i = 0; i < excludePatterns.Length; i++)
|
||||
foreach (var excludePattern in excludeTokenizer)
|
||||
{
|
||||
trimmedExcludePatterns.Add(TrimLeadingTildeSlash(excludePatterns[i]));
|
||||
trimmedExcludePatterns.Add(NormalizePath(excludePattern));
|
||||
}
|
||||
matcher.AddExcludePatterns(trimmedExcludePatterns);
|
||||
}
|
||||
|
||||
var matches = matcher.Execute(_baseGlobbingDirectory);
|
||||
|
||||
return matches.Files.Select(ResolveMatchedPath)
|
||||
.OrderBy(path => path, DefaultPathComparer);
|
||||
return Cache.Set<List<string>>(
|
||||
cacheKey,
|
||||
FindFiles(matcher),
|
||||
options);
|
||||
}
|
||||
|
||||
private string ResolveMatchedPath(FilePatternMatch matchedPath)
|
||||
private List<string> FindFiles(Matcher matcher)
|
||||
{
|
||||
// Resolve the path to site root
|
||||
var relativePath = new PathString("/" + matchedPath.Path);
|
||||
return RequestPathBase.Add(relativePath).ToString();
|
||||
var matches = matcher.Execute(_baseGlobbingDirectory);
|
||||
var matchedUrls = new List<string>();
|
||||
foreach (var matchedPath in matches.Files)
|
||||
{
|
||||
// Resolve the path to site root
|
||||
var relativePath = new PathString("/" + matchedPath.Path);
|
||||
var matchedUrl = RequestPathBase.Add(relativePath).ToString();
|
||||
var index = matchedUrls.BinarySearch(matchedUrl, DefaultPathComparer);
|
||||
if (index < 0)
|
||||
{
|
||||
// Item doesn't already exist. Insert it.
|
||||
matchedUrls.Insert(~index, matchedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return matchedUrls;
|
||||
}
|
||||
|
||||
private class PathComparer : IComparer<string>
|
||||
|
|
@ -189,60 +206,180 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
yExtIndex = yExtIndex > ySlashIndex ? yExtIndex : -1;
|
||||
|
||||
// Get paths without their extensions, if they have one
|
||||
var xLength = xExtIndex >= 0 ? xExtIndex : x.Length;
|
||||
var yLength = yExtIndex >= 0 ? yExtIndex : y.Length;
|
||||
var compareLength = Math.Max(xLength, yLength);
|
||||
|
||||
// In the resulting sequence, we want shorter paths to appear prior to longer paths. For paths of equal
|
||||
// depth, we'll compare individual segments. The first segment that differs determines the result.
|
||||
// For e.g.
|
||||
// Foo.cshtml < Foo.xhtml
|
||||
// Bar.cshtml < Foo.cshtml
|
||||
// ZZ/z.txt < A/A/a.txt
|
||||
// ZZ/a/z.txt < ZZ/z/a.txt
|
||||
|
||||
if (string.Compare(x, 0, y, 0, compareLength, StringComparison.Ordinal) == 0)
|
||||
{
|
||||
// Only extension differs so just compare the extension
|
||||
if (xExtIndex >= 0 && yExtIndex >= 0)
|
||||
{
|
||||
var length = x.Length - xExtIndex;
|
||||
return string.Compare(x, xExtIndex, y, yExtIndex, length, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
return xExtIndex - yExtIndex;
|
||||
}
|
||||
|
||||
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))
|
||||
var result = 0;
|
||||
var xEnumerator = new StringTokenizer(xNoExt, '/').GetEnumerator();
|
||||
var yEnumerator = new StringTokenizer(yNoExt, '/').GetEnumerator();
|
||||
StringSegment xSegment;
|
||||
StringSegment ySegment;
|
||||
while (TryGetNextSegment(ref xEnumerator, out xSegment))
|
||||
{
|
||||
// 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)
|
||||
if (!TryGetNextSegment(ref yEnumerator, out ySegment))
|
||||
{
|
||||
return xToY;
|
||||
// Different path depths (right is shorter), so shallower path wins.
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
// Once we've determined that a segment differs, we need to ensure that the two paths
|
||||
// are of equal depth.
|
||||
continue;
|
||||
}
|
||||
|
||||
var length = Math.Max(xSegment.Length, ySegment.Length);
|
||||
result = string.Compare(
|
||||
xSegment.Buffer,
|
||||
xSegment.Offset,
|
||||
ySegment.Buffer,
|
||||
ySegment.Offset,
|
||||
length,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (TryGetNextSegment(ref yEnumerator, out ySegment))
|
||||
{
|
||||
// Different path depths (left is shorter). Shallower path wins.
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Segments are of equal length
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetNextSegment(ref StringTokenizer.Enumerator enumerator, out StringSegment segment)
|
||||
{
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
if (enumerator.Current.HasValue && enumerator.Current.Length > 0)
|
||||
{
|
||||
segment = enumerator.Current;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Should't get here, but if we do, hey, they're the same :)
|
||||
return 0;
|
||||
segment = default(StringSegment);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string TrimLeadingTildeSlash(string value)
|
||||
private static string NormalizePath(StringSegment value)
|
||||
{
|
||||
var result = value.Trim(ValidAttributeWhitespaceChars);
|
||||
|
||||
if (result.StartsWith("~/", StringComparison.Ordinal))
|
||||
if (!value.HasValue || value.Length == 0)
|
||||
{
|
||||
result = result.Substring(2);
|
||||
return null;
|
||||
}
|
||||
else if (result.StartsWith("/", StringComparison.Ordinal) ||
|
||||
result.StartsWith("\\", StringComparison.Ordinal))
|
||||
|
||||
value = Trim(value);
|
||||
if (value.StartsWith("~/", StringComparison.Ordinal))
|
||||
{
|
||||
value = new StringSegment(value.Buffer, value.Offset + 2, value.Length - 2);
|
||||
}
|
||||
else if (value.StartsWith("/", StringComparison.Ordinal) ||
|
||||
value.StartsWith("\\", StringComparison.Ordinal))
|
||||
{
|
||||
// Trim the leading slash as the matcher runs from the provided root only anyway
|
||||
result = result.Substring(1);
|
||||
value = new StringSegment(value.Buffer, value.Offset + 1, value.Length - 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
private static bool IsWhiteSpace(string value, int index)
|
||||
{
|
||||
for (var i = 0; i < ValidAttributeWhitespaceChars.Length; i++)
|
||||
{
|
||||
if (value[index] == ValidAttributeWhitespaceChars[i])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static StringSegment Trim(StringSegment value)
|
||||
{
|
||||
var offset = value.Offset;
|
||||
while (offset < value.Offset + value.Length)
|
||||
{
|
||||
if (!IsWhiteSpace(value.Buffer, offset))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
offset++;
|
||||
}
|
||||
|
||||
var trimmedEnd = value.Offset + value.Length - 1;
|
||||
while (trimmedEnd >= offset)
|
||||
{
|
||||
if (!IsWhiteSpace(value.Buffer, trimmedEnd))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
trimmedEnd--;
|
||||
}
|
||||
|
||||
return new StringSegment(value.Buffer, offset, trimmedEnd - offset + 1);
|
||||
}
|
||||
|
||||
private struct GlobbingUrlKey : IEquatable<GlobbingUrlKey>
|
||||
{
|
||||
public GlobbingUrlKey(string include, string exclude)
|
||||
{
|
||||
Include = include;
|
||||
Exclude = exclude;
|
||||
}
|
||||
|
||||
public string Include { get; }
|
||||
|
||||
public string Exclude { get; }
|
||||
|
||||
public bool Equals(GlobbingUrlKey other)
|
||||
{
|
||||
return string.Equals(Include, other.Include, StringComparison.Ordinal) &&
|
||||
string.Equals(Exclude, other.Exclude, StringComparison.Ordinal);
|
||||
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashCodeCombiner = HashCodeCombiner.Start();
|
||||
hashCodeCombiner.Add(Include);
|
||||
hashCodeCombiner.Add(Exclude);
|
||||
|
||||
return hashCodeCombiner.CombinedHash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Methods for encoding <c>string[]</c>s for use as a JavaScript array literal.
|
||||
/// </summary>
|
||||
public static class JavaScriptStringArrayEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Encodes a <c>string[]</c> for safe use as a JavaScript array literal in many contexts, including
|
||||
/// inline in an HTML file.
|
||||
/// </summary>
|
||||
public static string Encode(JavaScriptEncoder encoder, string[] values)
|
||||
{
|
||||
var writer = new StringWriter();
|
||||
writer.Write('[');
|
||||
|
||||
// Perf: Avoid allocating enumerator
|
||||
var firstAdded = false;
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (firstAdded)
|
||||
{
|
||||
writer.Write(',');
|
||||
}
|
||||
|
||||
writer.Write('"');
|
||||
encoder.Encode(writer, values[i]);
|
||||
writer.Write('"');
|
||||
firstAdded = true;
|
||||
}
|
||||
|
||||
writer.Write(']');
|
||||
|
||||
return writer.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,14 +2,16 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNet.Hosting;
|
||||
using Microsoft.AspNet.Mvc.Razor.TagHelpers;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
||||
using Microsoft.AspNet.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNet.Razor.TagHelpers;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
|
|
@ -288,8 +290,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
|
||||
// Build a <link /> tag for each matched href.
|
||||
var urls = GlobbingUrlBuilder.BuildUrlList(null, HrefInclude, HrefExclude);
|
||||
foreach (var url in urls)
|
||||
for (var i = 0; i < urls.Count; i++)
|
||||
{
|
||||
var url = urls[i];
|
||||
|
||||
// "url" values come from bound attributes and globbing. Must always be non-null.
|
||||
Debug.Assert(url != null);
|
||||
|
||||
|
|
@ -307,45 +311,70 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
private void BuildFallbackBlock(TagHelperContent builder)
|
||||
{
|
||||
EnsureGlobbingUrlBuilder();
|
||||
var fallbackHrefs =
|
||||
GlobbingUrlBuilder.BuildUrlList(FallbackHref, FallbackHrefInclude, FallbackHrefExclude).ToArray();
|
||||
var fallbackHrefs = GlobbingUrlBuilder.BuildUrlList(
|
||||
FallbackHref,
|
||||
FallbackHrefInclude,
|
||||
FallbackHrefExclude);
|
||||
|
||||
if (fallbackHrefs.Length > 0)
|
||||
if (fallbackHrefs.Count == 0)
|
||||
{
|
||||
if (AppendVersion == true)
|
||||
{
|
||||
for (var i = 0; i < fallbackHrefs.Length; i++)
|
||||
{
|
||||
// fallbackHrefs come from bound attributes and globbing. Must always be non-null.
|
||||
Debug.Assert(fallbackHrefs[i] != null);
|
||||
return;
|
||||
}
|
||||
|
||||
fallbackHrefs[i] = _fileVersionProvider.AddFileVersionToPath(fallbackHrefs[i]);
|
||||
}
|
||||
builder.AppendHtml(HtmlString.NewLine);
|
||||
|
||||
// Build the <meta /> tag that's used to test for the presence of the stylesheet
|
||||
builder
|
||||
.AppendHtml("<meta name=\"x-stylesheet-fallback-test\" content=\"\" class=\"")
|
||||
.Append(FallbackTestClass)
|
||||
.AppendHtml("\" />");
|
||||
|
||||
// Build the <script /> tag that checks the effective style of <meta /> tag above and renders the extra
|
||||
// <link /> tag to load the fallback stylesheet if the test CSS property value is found to be false,
|
||||
// indicating that the primary stylesheet failed to load.
|
||||
// GetEmbeddedJavaScript returns JavaScript to which we add '"{0}","{1}",{2});'
|
||||
builder
|
||||
.AppendHtml("<script>")
|
||||
.AppendHtml(JavaScriptResources.GetEmbeddedJavaScript(FallbackJavaScriptResourceName))
|
||||
.AppendHtml("\"")
|
||||
.AppendHtml(JavaScriptEncoder.Encode(FallbackTestProperty))
|
||||
.AppendHtml("\",\"")
|
||||
.AppendHtml(JavaScriptEncoder.Encode(FallbackTestValue))
|
||||
.AppendHtml("\",");
|
||||
|
||||
AppendFallbackHrefs(builder, fallbackHrefs);
|
||||
builder.AppendHtml("</script>");
|
||||
}
|
||||
|
||||
private void AppendFallbackHrefs(TagHelperContent builder, IReadOnlyList<string> fallbackHrefs)
|
||||
{
|
||||
builder.AppendHtml("[");
|
||||
var firstAdded = false;
|
||||
// Perf: Avoid allocating enumerator
|
||||
for (var i = 0; i < fallbackHrefs.Count; i++)
|
||||
{
|
||||
if (firstAdded)
|
||||
{
|
||||
builder.AppendHtml(",\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AppendHtml("\"");
|
||||
firstAdded = true;
|
||||
}
|
||||
|
||||
builder.AppendHtml(HtmlString.NewLine);
|
||||
// fallbackHrefs come from bound attributes and globbing. Must always be non-null.
|
||||
Debug.Assert(fallbackHrefs[i] != null);
|
||||
var valueToWrite = fallbackHrefs[i];
|
||||
if (AppendVersion == true)
|
||||
{
|
||||
valueToWrite = _fileVersionProvider.AddFileVersionToPath(fallbackHrefs[i]);
|
||||
}
|
||||
|
||||
// Build the <meta /> tag that's used to test for the presence of the stylesheet
|
||||
builder
|
||||
.AppendHtml("<meta name=\"x-stylesheet-fallback-test\" content=\"\" class=\"")
|
||||
.Append(FallbackTestClass)
|
||||
.AppendHtml("\" />");
|
||||
|
||||
// Build the <script /> tag that checks the effective style of <meta /> tag above and renders the extra
|
||||
// <link /> tag to load the fallback stylesheet if the test CSS property value is found to be false,
|
||||
// indicating that the primary stylesheet failed to load.
|
||||
// GetEmbeddedJavaScript returns JavaScript to which we add '"{0}","{1}",{2});'
|
||||
builder
|
||||
.AppendHtml("<script>")
|
||||
.AppendHtml(JavaScriptResources.GetEmbeddedJavaScript(FallbackJavaScriptResourceName))
|
||||
.AppendHtml("\"")
|
||||
.AppendHtml(JavaScriptEncoder.Encode(FallbackTestProperty))
|
||||
.AppendHtml("\",\"")
|
||||
.AppendHtml(JavaScriptEncoder.Encode(FallbackTestValue))
|
||||
.AppendHtml("\",")
|
||||
.AppendHtml(JavaScriptStringArrayEncoder.Encode(JavaScriptEncoder, fallbackHrefs))
|
||||
.AppendHtml(");</script>");
|
||||
builder.AppendHtml(JavaScriptEncoder.Encode(valueToWrite));
|
||||
builder.AppendHtml("\"");
|
||||
}
|
||||
builder.AppendHtml("]);");
|
||||
}
|
||||
|
||||
private void EnsureGlobbingUrlBuilder()
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@
|
|||
"Microsoft.Extensions.PropertyHelper.Sources": {
|
||||
"version": "1.0.0-*",
|
||||
"type": "build"
|
||||
},
|
||||
"Microsoft.Extensions.HashCodeCombiner.Sources": {
|
||||
"version": "1.0.0-*",
|
||||
"type": "build"
|
||||
}
|
||||
},
|
||||
"frameworks": {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider();
|
||||
IMemoryCache cache = null;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var requestPathBase = PathString.Empty;
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
|
||||
IMemoryCache cache = null;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var requestPathBase = PathString.Empty;
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
|
|
@ -210,7 +210,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider(dirStructure);
|
||||
IMemoryCache cache = null;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var requestPathBase = PathString.Empty;
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
|
||||
IMemoryCache cache = null;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var requestPathBase = new PathString(pathBase);
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
|
|
@ -277,10 +277,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
Mock.Get(fileProvider).Setup(f => f.Watch(It.IsAny<string>())).Returns(changeToken.Object);
|
||||
var cache = MakeCache();
|
||||
Mock.Get(cache).Setup(c => c.Set(
|
||||
/*key*/ It.IsAny<string>(),
|
||||
/*value*/ It.IsAny<object>(),
|
||||
/*key*/ It.IsAny<object>(),
|
||||
/*value*/ It.IsAny<List<string>>(),
|
||||
/*options*/ It.IsAny<MemoryCacheEntryOptions>()))
|
||||
.Returns(new object())
|
||||
.Returns((object key, object value, MemoryCacheEntryOptions options) => value)
|
||||
.Verifiable();
|
||||
var requestPathBase = PathString.Empty;
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
|
@ -339,7 +339,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.js", "site2.txt", "site.js"));
|
||||
IMemoryCache cache = null;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var requestPathBase = PathString.Empty;
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
|
|
@ -363,7 +363,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
|
||||
IMemoryCache cache = null;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var requestPathBase = PathString.Empty;
|
||||
var includePatterns = new List<string>();
|
||||
var excludePatterns = new List<string>();
|
||||
|
|
@ -391,7 +391,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
// Arrange
|
||||
var leadingSlashes = $"{prefix}{prefix}";
|
||||
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
|
||||
IMemoryCache cache = null;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var requestPathBase = PathString.Empty;
|
||||
var includePatterns = new List<string>();
|
||||
var excludePatterns = new List<string>();
|
||||
|
|
@ -448,6 +448,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
var fileProvider = new Mock<IFileProvider>(MockBehavior.Strict);
|
||||
fileProvider.Setup(fp => fp.GetDirectoryContents(string.Empty))
|
||||
.Returns(MakeDirectoryContents(rootNode, fileProvider));
|
||||
fileProvider.Setup(fp => fp.Watch(It.IsAny<string>()))
|
||||
.Returns(new TestFileChangeToken());
|
||||
|
||||
return fileProvider.Object;
|
||||
}
|
||||
|
|
@ -494,13 +496,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
var fileProvider = new Mock<IFileProvider>();
|
||||
fileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny<string>()))
|
||||
.Returns(directoryContents);
|
||||
fileProvider.Setup(fp => fp.Watch(It.IsAny<string>()))
|
||||
.Returns(new TestFileChangeToken());
|
||||
return fileProvider.Object;
|
||||
}
|
||||
|
||||
private static IMemoryCache MakeCache(object result = null)
|
||||
{
|
||||
var cache = new Mock<IMemoryCache>();
|
||||
cache.Setup(c => c.TryGetValue(It.IsAny<string>(), out result))
|
||||
cache.Setup(c => c.TryGetValue(It.IsAny<object>(), out result))
|
||||
.Returns(result != null);
|
||||
return cache.Object;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ using System.Text;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.AspNet.Hosting;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Http.Internal;
|
||||
using Microsoft.AspNet.Mvc.Abstractions;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.Razor;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
||||
|
|
@ -279,7 +281,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var output = MakeTagHelperOutput("link");
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
|
||||
new TestFileProvider(),
|
||||
Mock.Of<IMemoryCache>(),
|
||||
PathString.Empty);
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns(new[] { "/common.css" });
|
||||
|
||||
|
|
@ -373,7 +378,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var output = MakeTagHelperOutput("link");
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
|
||||
new TestFileProvider(),
|
||||
Mock.Of<IMemoryCache>(),
|
||||
PathString.Empty);
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns(new[] { "/common.css" });
|
||||
|
||||
|
|
@ -597,7 +605,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
});
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
|
||||
new TestFileProvider(),
|
||||
Mock.Of<IMemoryCache>(),
|
||||
PathString.Empty);
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.css", null))
|
||||
.Returns(new[] { "/base.css" });
|
||||
|
||||
|
|
@ -640,7 +651,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
});
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
|
||||
new TestFileProvider(),
|
||||
Mock.Of<IMemoryCache>(),
|
||||
PathString.Empty);
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.css", null))
|
||||
.Returns(new[] { "/base.css" });
|
||||
|
||||
|
|
@ -761,7 +775,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
});
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
|
||||
new TestFileProvider(),
|
||||
Mock.Of<IMemoryCache>(),
|
||||
PathString.Empty);
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.css", null))
|
||||
.Returns(new[] { "/base.css" });
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ using System.Text;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.AspNet.Hosting;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Http.Internal;
|
||||
using Microsoft.AspNet.Mvc.Abstractions;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.Razor;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
||||
|
|
@ -272,7 +274,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var output = MakeTagHelperOutput("script");
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
|
||||
new TestFileProvider(),
|
||||
Mock.Of<IMemoryCache>(),
|
||||
PathString.Empty);
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns(new[] { "/common.js" });
|
||||
|
||||
|
|
@ -366,7 +371,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var output = MakeTagHelperOutput("script");
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
|
||||
new TestFileProvider(),
|
||||
Mock.Of<IMemoryCache>(),
|
||||
PathString.Empty);
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns(new[] { "/common.js" });
|
||||
|
||||
|
|
@ -573,7 +581,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
|
||||
new TestFileProvider(),
|
||||
Mock.Of<IMemoryCache>(),
|
||||
PathString.Empty);
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.js", null))
|
||||
.Returns(new[] { "/common.js" });
|
||||
|
||||
|
|
@ -612,7 +623,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
|
||||
new TestFileProvider(),
|
||||
Mock.Of<IMemoryCache>(),
|
||||
PathString.Empty);
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.js", null))
|
||||
.Returns(new[] { "/common.js" });
|
||||
|
||||
|
|
@ -762,7 +776,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var output = MakeTagHelperOutput("script", attributes: new TagHelperAttributeList());
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>(
|
||||
new TestFileProvider(),
|
||||
Mock.Of<IMemoryCache>(),
|
||||
PathString.Empty);
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "*.js", null))
|
||||
.Returns(new[] { "/common.js" });
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue