parent
bcbbc58515
commit
6e845f0171
|
|
@ -1,12 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="~/blank.css" rel="stylesheet" />
|
||||
<link href="~/site.min.css" rel="stylesheet"
|
||||
asp-fallback-href="~/site.css"
|
||||
<link href="~/css/sub/blank.css" asp-href-include="**/blank.css" rel="stylesheet" data-test="value" />
|
||||
<link href="site.min.css" rel="stylesheet" data-test="value"
|
||||
asp-fallback-href-include="**/site.css"
|
||||
asp-fallback-test-class="fallback-test"
|
||||
asp-fallback-test-property="visibility"
|
||||
asp-fallback-test-value="hidden" />
|
||||
<link asp-href-include="../*.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
@RenderBody()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
body::after {
|
||||
display: block;
|
||||
background-color: #ff0000;
|
||||
color: #fff;
|
||||
font-size: 1.2em;
|
||||
margin-top: 2.4em;
|
||||
content: "ERROR: Stylesheet 'appRoot.css' was loaded from outside webroot!";
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"commands": {
|
||||
"web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001",
|
||||
"web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001/taghelpers",
|
||||
"kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000"
|
||||
},
|
||||
"compilationOptions": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
body {}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// 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.Linq;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.Framework.Logging;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Methods for determining how an <see cref="ITagHelper"/> should run based on the attributes that were specified.
|
||||
/// </summary>
|
||||
public static class AttributeMatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether a <see cref="ITagHelper" />'s required attributes are present, non null, non empty, and
|
||||
/// non whitepsace.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="TagHelperContext"/>.</param>
|
||||
/// <param name="requiredAttributes">
|
||||
/// The attributes the <see cref="ITagHelper" /> requires in order to run.
|
||||
/// </param>
|
||||
/// <param name="logger">An optional <see cref="ILogger"/> to log warning details to.</param>
|
||||
/// <returns>A <see cref="bool"/> indicating whether the <see cref="ITagHelper" /> should run.</returns>
|
||||
public static bool AllRequiredAttributesArePresent(
|
||||
[NotNull] TagHelperContext context,
|
||||
[NotNull] IEnumerable<string> requiredAttributes,
|
||||
ILogger logger)
|
||||
{
|
||||
var attributes = GetPresentMissingAttributes(context, requiredAttributes);
|
||||
|
||||
if (attributes.Missing.Any())
|
||||
{
|
||||
if (attributes.Present.Any() && logger != null && logger.IsEnabled(LogLevel.Warning))
|
||||
{
|
||||
// At least 1 attribute was present indicating the user intended to use the tag helper,
|
||||
// but at least 1 was missing too, so log a warning with the details.
|
||||
logger.WriteWarning(new MissingAttributeLoggerStructure(context.UniqueId, attributes.Missing));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// All required attributes present
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the modes a <see cref="ITagHelper" /> can run in based on which modes have all their required
|
||||
/// attributes present, non null, non empty, and non whitepsace.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMode">The type representing the <see cref="ITagHelper" />'s modes.</typeparam>
|
||||
/// <param name="context">The <see cref="TagHelperContext"/>.</param>
|
||||
/// <param name="modeInfos">The modes and their required attributes.</param>
|
||||
/// <returns>The <see cref="ModeMatchResult{TMode}"/>.</returns>
|
||||
public static ModeMatchResult<TMode> DetermineMode<TMode>(
|
||||
[NotNull] TagHelperContext context,
|
||||
[NotNull] IEnumerable<ModeAttributes<TMode>> modeInfos)
|
||||
{
|
||||
// true == full match, false == partial match
|
||||
var matchedAttributes = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
var result = new ModeMatchResult<TMode>();
|
||||
|
||||
foreach (var modeInfo in modeInfos)
|
||||
{
|
||||
var modeAttributes = GetPresentMissingAttributes(context, modeInfo.Attributes);
|
||||
|
||||
if (modeAttributes.Present.Any())
|
||||
{
|
||||
if (!modeAttributes.Missing.Any())
|
||||
{
|
||||
// A complete match, mark the attribute as fully matched
|
||||
foreach (var attribute in modeAttributes.Present)
|
||||
{
|
||||
matchedAttributes[attribute] = true;
|
||||
}
|
||||
|
||||
result.FullMatches.Add(ModeMatchAttributes.Create(modeInfo.Mode, modeInfo.Attributes));
|
||||
}
|
||||
else
|
||||
{
|
||||
// A partial match, mark the attribute as partially matched if not already fully matched
|
||||
foreach (var attribute in modeAttributes.Present)
|
||||
{
|
||||
bool attributeMatch;
|
||||
if (!matchedAttributes.TryGetValue(attribute, out attributeMatch))
|
||||
{
|
||||
matchedAttributes[attribute] = false;
|
||||
}
|
||||
}
|
||||
|
||||
result.PartialMatches.Add(ModeMatchAttributes.Create(
|
||||
modeInfo.Mode, modeAttributes.Present, modeAttributes.Missing));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the list of partially matched attributes (those with partial matches but no full matches)
|
||||
foreach (var attribute in matchedAttributes.Keys)
|
||||
{
|
||||
if (!matchedAttributes[attribute])
|
||||
{
|
||||
result.PartiallyMatchedAttributes.Add(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static PresentMissingAttributes GetPresentMissingAttributes(
|
||||
TagHelperContext context,
|
||||
IEnumerable<string> requiredAttributes)
|
||||
{
|
||||
// Check for all attribute values
|
||||
var presentAttributes = new List<string>();
|
||||
var missingAttributes = new List<string>();
|
||||
|
||||
foreach (var attribute in requiredAttributes)
|
||||
{
|
||||
if (!context.AllAttributes.ContainsKey(attribute) ||
|
||||
context.AllAttributes[attribute] == null ||
|
||||
string.IsNullOrWhiteSpace(context.AllAttributes[attribute] as string))
|
||||
{
|
||||
// Missing attribute!
|
||||
missingAttributes.Add(attribute);
|
||||
}
|
||||
else
|
||||
{
|
||||
presentAttributes.Add(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
return new PresentMissingAttributes { Present = presentAttributes, Missing = missingAttributes };
|
||||
}
|
||||
|
||||
private class PresentMissingAttributes
|
||||
{
|
||||
public IEnumerable<string> Present { get; set; }
|
||||
|
||||
public IEnumerable<string> Missing { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// 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.IO;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.Framework.FileSystemGlobbing.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
public class FileProviderGlobbingDirectory : DirectoryInfoBase
|
||||
{
|
||||
private const char DirectorySeparatorChar = '/';
|
||||
private readonly IFileProvider _fileProvider;
|
||||
private readonly IFileInfo _fileInfo;
|
||||
private readonly FileProviderGlobbingDirectory _parent;
|
||||
private readonly bool _isRoot;
|
||||
|
||||
public FileProviderGlobbingDirectory(
|
||||
[NotNull] IFileProvider fileProvider,
|
||||
IFileInfo fileInfo,
|
||||
FileProviderGlobbingDirectory parent)
|
||||
{
|
||||
_fileProvider = fileProvider;
|
||||
_fileInfo = fileInfo;
|
||||
_parent = parent;
|
||||
|
||||
if (_fileInfo == null)
|
||||
{
|
||||
// We're the root of the directory tree
|
||||
RelativePath = string.Empty;
|
||||
_isRoot = true;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(parent?.RelativePath))
|
||||
{
|
||||
// We have a parent and they have a relative path so concat that with my name
|
||||
RelativePath = _parent.RelativePath + DirectorySeparatorChar + _fileInfo.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We have a parent which is the root, so just use my name
|
||||
RelativePath = _fileInfo.Name;
|
||||
}
|
||||
}
|
||||
|
||||
public string RelativePath { get; }
|
||||
|
||||
public override string FullName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isRoot)
|
||||
{
|
||||
// We're the root, so just use our name
|
||||
return Name;
|
||||
}
|
||||
|
||||
return _parent.FullName + DirectorySeparatorChar + Name;
|
||||
}
|
||||
}
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return _fileInfo?.Name;
|
||||
}
|
||||
}
|
||||
|
||||
public override DirectoryInfoBase ParentDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return _parent;
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<FileSystemInfoBase> EnumerateFileSystemInfos(
|
||||
string searchPattern,
|
||||
SearchOption searchOption)
|
||||
{
|
||||
if (!string.Equals(searchPattern, "*", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Only * based searches are ever performed against this API and we have an item to change this API
|
||||
// such that the searchPattern doesn't even get passed in, so this is just a safe-guard until then.
|
||||
// The searchPattern here has no relation to the globbing pattern.
|
||||
throw new ArgumentException(
|
||||
"Only full wildcard searches are supported, i.e. \"*\".",
|
||||
nameof(searchPattern));
|
||||
}
|
||||
|
||||
if (searchOption != SearchOption.TopDirectoryOnly)
|
||||
{
|
||||
// Only SearchOption.TopDirectoryOnly is actually used in the implementation of Matcher and will likely
|
||||
// be removed from DirectoryInfoBase in the near future. This is just a safe-guard until then.
|
||||
// The searchOption here has no relation to the globbing pattern.
|
||||
throw new ArgumentException(
|
||||
$"Only {nameof(SearchOption.TopDirectoryOnly)} is supported.",
|
||||
nameof(searchOption));
|
||||
}
|
||||
|
||||
|
||||
|
||||
foreach (var fileInfo in _fileProvider.GetDirectoryContents(RelativePath))
|
||||
{
|
||||
yield return BuildFileResult(fileInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public override DirectoryInfoBase GetDirectory(string path)
|
||||
{
|
||||
return new FileProviderGlobbingDirectory(_fileProvider, _fileProvider.GetFileInfo(path), this);
|
||||
}
|
||||
|
||||
public override FileInfoBase GetFile(string path)
|
||||
{
|
||||
return new FileProviderGlobbingFile(_fileProvider.GetFileInfo(path), this);
|
||||
}
|
||||
|
||||
private FileSystemInfoBase BuildFileResult(IFileInfo fileInfo)
|
||||
{
|
||||
if (fileInfo.IsDirectory)
|
||||
{
|
||||
return new FileProviderGlobbingDirectory(_fileProvider, fileInfo, this);
|
||||
}
|
||||
|
||||
return new FileProviderGlobbingFile(fileInfo, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.Framework.FileSystemGlobbing.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
public class FileProviderGlobbingFile : FileInfoBase
|
||||
{
|
||||
private const char DirectorySeparatorChar = '/';
|
||||
|
||||
public FileProviderGlobbingFile([NotNull] IFileInfo fileInfo, [NotNull] DirectoryInfoBase parent)
|
||||
{
|
||||
Name = fileInfo.Name;
|
||||
ParentDirectory = parent;
|
||||
FullName = ParentDirectory.FullName + DirectorySeparatorChar + Name;
|
||||
}
|
||||
|
||||
public override string FullName { get; }
|
||||
|
||||
public override string Name { get; }
|
||||
|
||||
public override DirectoryInfoBase ParentDirectory { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// 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.Linq;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.Framework.Cache.Memory;
|
||||
using Microsoft.Framework.FileSystemGlobbing;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility methods for <see cref="ITagHelper"/>'s that support attributes containing file globbing patterns.
|
||||
/// </summary>
|
||||
public class GlobbingUrlBuilder
|
||||
{
|
||||
private static readonly char[] PatternSeparator = new[] { ',' };
|
||||
|
||||
private readonly FileProviderGlobbingDirectory _baseGlobbingDirectory;
|
||||
|
||||
// Internal for testing
|
||||
internal GlobbingUrlBuilder() { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="GlobbingUrlBuilder"/>.
|
||||
/// </summary>
|
||||
/// <param name="fileProvider">The file provider.</param>
|
||||
/// <param name="cache">The cache.</param>
|
||||
/// <param name="requestPathBase">The request path base.</param>
|
||||
public GlobbingUrlBuilder([NotNull] IFileProvider fileProvider, IMemoryCache cache, PathString requestPathBase)
|
||||
{
|
||||
FileProvider = fileProvider;
|
||||
Cache = cache;
|
||||
RequestPathBase = requestPathBase;
|
||||
_baseGlobbingDirectory = new FileProviderGlobbingDirectory(fileProvider, fileInfo: null, parent: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IMemoryCache"/> to cache globbing results in.
|
||||
/// </summary>
|
||||
public IMemoryCache Cache { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IFileProvider"/> used to watch for changes to file globbing results.
|
||||
/// </summary>
|
||||
public IFileProvider FileProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The base path of the current request (i.e. <see cref="HttpRequest.PathBase"/>).
|
||||
/// </summary>
|
||||
public PathString RequestPathBase { get; }
|
||||
|
||||
// Internal for testing.
|
||||
internal Func<Matcher> MatcherBuilder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Builds a list of URLs.
|
||||
/// </summary>
|
||||
/// <param name="staticUrl">The statically declared URL. This will always be added to the result.</param>
|
||||
/// <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 IEnumerable<string> BuildUrlList(string staticUrl, string includePattern, string excludePattern)
|
||||
{
|
||||
var urls = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
// Add the statically declared url if present
|
||||
if (staticUrl != null)
|
||||
{
|
||||
urls.Add(staticUrl);
|
||||
}
|
||||
|
||||
// Add urls that match the globbing patterns specified
|
||||
var matchedUrls = ExpandGlobbedUrl(includePattern, excludePattern);
|
||||
urls.UnionWith(matchedUrls);
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
private IEnumerable<string> ExpandGlobbedUrl(string include, string exclude)
|
||||
{
|
||||
if (string.IsNullOrEmpty(include))
|
||||
{
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
var includePatterns = include.Split(PatternSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
var excludePatterns = exclude?.Split(PatternSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (includePatterns.Length == 0)
|
||||
{
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
if (Cache != null)
|
||||
{
|
||||
var cacheKey = $"{nameof(GlobbingUrlBuilder)}-inc:{include}-exc:{exclude}";
|
||||
return Cache.GetOrSet(cacheKey, cacheSetContext =>
|
||||
{
|
||||
foreach (var pattern in includePatterns)
|
||||
{
|
||||
var trigger = FileProvider.Watch(pattern);
|
||||
cacheSetContext.AddExpirationTrigger(trigger);
|
||||
}
|
||||
|
||||
return FindFiles(includePatterns, excludePatterns);
|
||||
});
|
||||
}
|
||||
|
||||
return FindFiles(includePatterns, excludePatterns);
|
||||
}
|
||||
|
||||
private IEnumerable<string> FindFiles(IEnumerable<string> includePatterns, IEnumerable<string> excludePatterns)
|
||||
{
|
||||
var matcher = MatcherBuilder != null ? MatcherBuilder() : new Matcher();
|
||||
|
||||
matcher.AddIncludePatterns(includePatterns.Select(pattern => TrimLeadingSlash(pattern)));
|
||||
|
||||
if (excludePatterns != null)
|
||||
{
|
||||
matcher.AddExcludePatterns(excludePatterns.Select(pattern => TrimLeadingSlash(pattern)));
|
||||
}
|
||||
|
||||
var matches = matcher.Execute(_baseGlobbingDirectory);
|
||||
|
||||
return matches.Files.Select(ResolveMatchedPath);
|
||||
}
|
||||
|
||||
private string ResolveMatchedPath(string matchedPath)
|
||||
{
|
||||
// Resolve the path to site root
|
||||
var relativePath = new PathString("/" + matchedPath);
|
||||
return RequestPathBase.Add(relativePath).ToString();
|
||||
}
|
||||
|
||||
private static string TrimLeadingSlash(string value)
|
||||
{
|
||||
var result = value;
|
||||
|
||||
if (result.StartsWith("/", StringComparison.Ordinal) ||
|
||||
result.StartsWith("\\", StringComparison.Ordinal))
|
||||
{
|
||||
// Trim the leading slash as the matcher runs from the provided root only anyway
|
||||
result = result.Substring(1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Methods for loading JavaScript from assembly embedded resources.
|
||||
/// </summary>
|
||||
public static class JavaScriptResources
|
||||
{
|
||||
private static readonly Assembly ResourcesAssembly = typeof(JavaScriptResources).GetTypeInfo().Assembly;
|
||||
|
||||
private static readonly ConcurrentDictionary<string, string> Cache =
|
||||
new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an embedded JavaScript file resource and decodes it for use as a .NET format string.
|
||||
/// </summary>
|
||||
public static string GetEmbeddedJavaScript(string resourceName)
|
||||
{
|
||||
return GetEmbeddedJavaScript(resourceName, ResourcesAssembly.GetManifestResourceStream, Cache);
|
||||
}
|
||||
|
||||
// Internal for testing
|
||||
internal static string GetEmbeddedJavaScript(
|
||||
string resourceName,
|
||||
Func<string, Stream> getManifestResourceStream,
|
||||
ConcurrentDictionary<string, string> cache)
|
||||
{
|
||||
return cache.GetOrAdd(resourceName, key =>
|
||||
{
|
||||
// Load the JavaScript from embedded resource
|
||||
using (var resourceStream = getManifestResourceStream(key))
|
||||
{
|
||||
Debug.Assert(resourceStream != null, "Embedded resource missing. Ensure 'prebuild' script has run.");
|
||||
|
||||
using (var streamReader = new StreamReader(resourceStream))
|
||||
{
|
||||
var script = streamReader.ReadToEnd();
|
||||
|
||||
return PrepareFormatString(script);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static string PrepareFormatString(string input)
|
||||
{
|
||||
// Replace unescaped/escaped chars with their equivalent
|
||||
return input.Replace("{", "{{")
|
||||
.Replace("}", "}}")
|
||||
.Replace("[[[", "{")
|
||||
.Replace("]]]", "}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.WebUtilities.Encoders;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Methods for encoding <see cref="IEnumerable{String}"/> for use as a JavaScript array literal.
|
||||
/// </summary>
|
||||
public static class JavaScriptStringArrayEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Encodes a .NET string array for safe use as a JavaScript array literal, including inline in an HTML file.
|
||||
/// </summary>
|
||||
public static string Encode(IJavaScriptStringEncoder encoder, IEnumerable<string> values)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
var firstAdded = false;
|
||||
|
||||
builder.Append('[');
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (firstAdded)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
builder.Append('"');
|
||||
builder.Append(encoder.JavaScriptStringEncode(value));
|
||||
builder.Append('"');
|
||||
firstAdded = true;
|
||||
}
|
||||
|
||||
builder.Append(']');
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,10 +5,10 @@ using System.Collections.Generic;
|
|||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.Framework.Logging;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ILoggerStructure"/> for log messages regarding <see cref="TagHelper"/> instances that opt out of
|
||||
/// An <see cref="ILoggerStructure"/> for log messages regarding <see cref="ITagHelper"/> instances that opt out of
|
||||
/// processing due to missing required attributes.
|
||||
/// </summary>
|
||||
public class MissingAttributeLoggerStructure : ILoggerStructure
|
||||
|
|
@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
private readonly string _uniqueId;
|
||||
private readonly IEnumerable<KeyValuePair<string, object>> _values;
|
||||
|
||||
// internal for unit testing.
|
||||
// Internal for unit testing
|
||||
internal IEnumerable<string> MissingAttributes { get; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -30,8 +30,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
MissingAttributes = missingAttributes;
|
||||
_values = new Dictionary<string, object>
|
||||
{
|
||||
{ "UniqueId", _uniqueId },
|
||||
{ "MissingAttributes", MissingAttributes }
|
||||
["UniqueId"] = _uniqueId,
|
||||
["MissingAttributes"] = MissingAttributes
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
{
|
||||
get
|
||||
{
|
||||
return "Tag Helper skipped due to missing required attributes.";
|
||||
return "Tag Helper has one or more missing required attributes.";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Static creation methods for <see cref="ModeAttributes{TMode}"/>.
|
||||
/// </summary>
|
||||
public static class ModeAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ModeAttributes{TMode}"/>/
|
||||
/// </summary>
|
||||
public static ModeAttributes<TMode> Create<TMode>(TMode mode, IEnumerable<string> attributes)
|
||||
{
|
||||
return new ModeAttributes<TMode>
|
||||
{
|
||||
Mode = mode,
|
||||
Attributes = attributes
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// A mapping of a <see cref="ITagHelper"/> mode to its required attributes.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMode">The type representing the <see cref="ITagHelper"/>'s mode.</typeparam>
|
||||
public class ModeAttributes<TMode>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ITagHelper"/>'s mode.
|
||||
/// </summary>
|
||||
public TMode Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The names of attributes required for this mode.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Attributes { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Static creation methods for <see cref="ModeMatchAttributes{TMode}"/>.
|
||||
/// </summary>
|
||||
public static class ModeMatchAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ModeMatchAttributes{TMode}"/>.
|
||||
/// </summary>
|
||||
public static ModeMatchAttributes<TMode> Create<TMode>(
|
||||
TMode mode,
|
||||
IEnumerable<string> presentAttributes)
|
||||
{
|
||||
return Create(mode, presentAttributes, missingAttributes: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ModeMatchAttributes{TMode}"/>.
|
||||
/// </summary>
|
||||
public static ModeMatchAttributes<TMode> Create<TMode>(
|
||||
TMode mode,
|
||||
IEnumerable<string> presentAttributes,
|
||||
IEnumerable<string> missingAttributes)
|
||||
{
|
||||
return new ModeMatchAttributes<TMode>
|
||||
{
|
||||
Mode = mode,
|
||||
PresentAttributes = presentAttributes,
|
||||
MissingAttributes = missingAttributes
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// A mapping of a <see cref="ITagHelper"/> mode to its missing and present attributes.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMode">The type representing the <see cref="ITagHelper"/>'s mode.</typeparam>
|
||||
public class ModeMatchAttributes<TMode>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ITagHelper"/>'s mode.
|
||||
/// </summary>
|
||||
public TMode Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The names of attributes that were present in this match.
|
||||
/// </summary>
|
||||
public IEnumerable<string> PresentAttributes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The names of attributes that were missing in this match.
|
||||
/// </summary>
|
||||
public IEnumerable<string> MissingAttributes { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// 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.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.Framework.Logging;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Result of determining the mode an <see cref="ITagHelper"/> will run in.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMode">The type representing the <see cref="ITagHelper"/>'s mode.</typeparam>
|
||||
public class ModeMatchResult<TMode>
|
||||
{
|
||||
/// <summary>
|
||||
/// Modes that were missing attributes but had at least one attribute present.
|
||||
/// </summary>
|
||||
public IList<ModeMatchAttributes<TMode>> PartialMatches { get; } = new List<ModeMatchAttributes<TMode>>();
|
||||
|
||||
/// <summary>
|
||||
/// Modes that had all attributes present.
|
||||
/// </summary>
|
||||
public IList<ModeMatchAttributes<TMode>> FullMatches { get; } = new List<ModeMatchAttributes<TMode>>();
|
||||
|
||||
/// <summary>
|
||||
/// Attributes that are present in at least one mode in <see cref="PartialMatches"/>, but in no modes in
|
||||
/// <see cref="FullMatches"/>.
|
||||
/// </summary>
|
||||
public IList<string> PartiallyMatchedAttributes { get; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Logs the details of the <see cref="ModeMatchResult{TMode}"/>.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||
/// <param name="tagHelper">The <see cref="ITagHelper"/>.</param>
|
||||
/// <param name="uniqueId">The value of <see cref="TagHelperContext.UniqueId"/>.</param>
|
||||
public void LogDetails<TTagHelper>([NotNull] ILogger logger, [NotNull] TTagHelper tagHelper, string uniqueId)
|
||||
where TTagHelper : ITagHelper
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Warning) && PartiallyMatchedAttributes.Any())
|
||||
{
|
||||
// Build the list of partial matches that contain attributes not appearing in at least one full match
|
||||
var partialOnlyMatches = PartialMatches.Where(
|
||||
match => match.PresentAttributes.Any(
|
||||
attribute => PartiallyMatchedAttributes.Contains(
|
||||
attribute, StringComparer.OrdinalIgnoreCase)));
|
||||
|
||||
logger.WriteWarning(new PartialModeMatchLoggerStructure<TMode>(uniqueId, partialOnlyMatches));
|
||||
}
|
||||
|
||||
if (logger.IsEnabled(LogLevel.Verbose) && !FullMatches.Any())
|
||||
{
|
||||
logger.WriteVerbose("Skipping processing for {0} {1}",
|
||||
tagHelper.GetType().GetTypeInfo().FullName, uniqueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// 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.Linq;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.Framework.Logging;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ILoggerStructure"/> for log messages regarding <see cref="ITagHelper"/> instances that opt out of
|
||||
/// processing due to missing attributes for one of several possible modes.
|
||||
/// </summary>
|
||||
public class PartialModeMatchLoggerStructure<TMode> : ILoggerStructure
|
||||
{
|
||||
private readonly string _uniqueId;
|
||||
private readonly IEnumerable<ModeMatchAttributes<TMode>> _partialMatches;
|
||||
private readonly IEnumerable<KeyValuePair<string, object>> _values;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PartialModeMatchLoggerStructure{TMode}"/>.
|
||||
/// </summary>
|
||||
/// <param name="uniqueId">The unique ID of the HTML element this message applies to.</param>
|
||||
/// <param name="partialMatches">The set of modes with partial required attributes.</param>
|
||||
public PartialModeMatchLoggerStructure(
|
||||
string uniqueId,
|
||||
[NotNull] IEnumerable<ModeMatchAttributes<TMode>> partialMatches)
|
||||
{
|
||||
_uniqueId = uniqueId;
|
||||
_partialMatches = partialMatches;
|
||||
_values = new Dictionary<string, object>
|
||||
{
|
||||
["UniqueId"] = _uniqueId,
|
||||
["PartialMatches"] = partialMatches
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The log message.
|
||||
/// </summary>
|
||||
public string Message
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Tag Helper has missing required attributes.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the values associated with this structured log message.
|
||||
/// </summary>
|
||||
/// <returns>The values.</returns>
|
||||
public IEnumerable<KeyValuePair<string, object>> GetValues()
|
||||
{
|
||||
return _values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a human readable string for this structured log message.
|
||||
/// </summary>
|
||||
/// <returns>The message.</returns>
|
||||
public string Format()
|
||||
{
|
||||
var newLine = Environment.NewLine;
|
||||
return
|
||||
string.Format($"Tag Helper {_uniqueId} had partial matches while determining mode:{newLine}\t{{0}}",
|
||||
string.Join($"{newLine}\t", _partialMatches.Select(partial =>
|
||||
string.Format($"Mode '{partial.Mode}' missing attributes:{newLine}\t\t{{0}} ",
|
||||
string.Join($"{newLine}\t\t", partial.MissingAttributes)))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility methods for dealing with JavaScript.
|
||||
/// </summary>
|
||||
public static class JavaScriptUtility
|
||||
{
|
||||
private static readonly Assembly ResourcesAssembly = typeof(JavaScriptUtility).GetTypeInfo().Assembly;
|
||||
|
||||
private static readonly ConcurrentDictionary<string, string> Cache =
|
||||
new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
private static readonly IDictionary<char, string> EncodingMap = new Dictionary<char, string>
|
||||
{
|
||||
{ '<', @"\u003c" }, // opening angle-bracket
|
||||
{ '>', @"\u003e" }, // closing angle-bracket
|
||||
{ '\'', @"\u0027" }, // single quote
|
||||
{ '"', @"\u0022" }, // double quote
|
||||
{ '\\', @"\\" }, // back slash
|
||||
{ '\r', "\\r" }, // carriage return
|
||||
{ '\n', "\\n" }, // new line
|
||||
{ '\u0085', @"\u0085" }, // next line
|
||||
{ '&', @"\u0026" }, // ampersand
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets an embedded JavaScript file resource and decodes it for use as a .NET format string.
|
||||
/// </summary>
|
||||
public static string GetEmbeddedJavaScript(string resourceName)
|
||||
{
|
||||
return Cache.GetOrAdd(resourceName, key =>
|
||||
{
|
||||
// Load the JavaScript from embedded resource
|
||||
using (var resourceStream = ResourcesAssembly.GetManifestResourceStream(key))
|
||||
{
|
||||
Debug.Assert(resourceStream != null, "Embedded resource missing. Ensure 'prebuild' script has run.");
|
||||
|
||||
using (var streamReader = new StreamReader(resourceStream))
|
||||
{
|
||||
var script = streamReader.ReadToEnd();
|
||||
|
||||
// Replace unescaped/escaped chars with their equivalent
|
||||
return PrepareFormatString(script);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Internal so we can test this separately
|
||||
internal static string PrepareFormatString(string input)
|
||||
{
|
||||
return input.Replace("{", "{{")
|
||||
.Replace("}", "}}")
|
||||
.Replace("[[[", "{")
|
||||
.Replace("]]]", "}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a .NET string for safe use as a JavaScript string literal, including inline in an HTML file.
|
||||
/// </summary>
|
||||
internal static string JavaScriptStringEncode(string value)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
|
||||
foreach (var character in value)
|
||||
{
|
||||
if (CharRequiresJavaScriptEncoding(character))
|
||||
{
|
||||
EncodeAndAppendChar(result, character);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append(character);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private static bool CharRequiresJavaScriptEncoding(char character)
|
||||
{
|
||||
return character < 0x20 // Control chars
|
||||
|| EncodingMap.ContainsKey(character);
|
||||
}
|
||||
|
||||
private static void EncodeAndAppendChar(StringBuilder builder, char character)
|
||||
{
|
||||
string mapped;
|
||||
|
||||
if (!EncodingMap.TryGetValue(character, out mapped))
|
||||
{
|
||||
mapped = "\\u" + ((int)character).ToString("x4", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
builder.Append(mapped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,17 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.Hosting;
|
||||
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.AspNet.WebUtilities.Encoders;
|
||||
using Microsoft.Framework.Cache.Memory;
|
||||
using Microsoft.Framework.Logging;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
|
|
@ -13,22 +21,77 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
/// </summary>
|
||||
public class LinkTagHelper : TagHelper
|
||||
{
|
||||
private const string HrefIncludeAttributeName = "asp-href-include";
|
||||
private const string HrefExcludeAttributeName = "asp-href-exclude";
|
||||
private const string FallbackHrefAttributeName = "asp-fallback-href";
|
||||
private const string FallbackHrefIncludeAttributeName = "asp-fallback-href-include";
|
||||
private const string FallbackHrefExcludeAttributeName = "asp-fallback-href-exclude";
|
||||
private const string FallbackTestClassAttributeName = "asp-fallback-test-class";
|
||||
private const string FallbackTestPropertyAttributeName = "asp-fallback-test-property";
|
||||
private const string FallbackTestValueAttributeName = "asp-fallback-test-value";
|
||||
private const string FallbackTestMetaTemplate = "<meta name=\"x-stylesheet-fallback-test\" class=\"{0}\" />";
|
||||
private const string FallbackJavaScriptResourceName = "compiler/resources/LinkTagHelper_FallbackJavaScript.js";
|
||||
|
||||
// NOTE: All attributes are required for the LinkTagHelper to process.
|
||||
private static readonly string[] RequiredAttributes = new[]
|
||||
{
|
||||
FallbackHrefAttributeName,
|
||||
FallbackTestClassAttributeName,
|
||||
FallbackTestPropertyAttributeName,
|
||||
FallbackTestValueAttributeName
|
||||
private static readonly ModeAttributes<Mode>[] ModeDetails = new[] {
|
||||
// Globbed Href (include only) no static href
|
||||
ModeAttributes.Create(Mode.GlobbedHref, new [] { HrefIncludeAttributeName }),
|
||||
// Globbed Href (include & exclude), no static href
|
||||
ModeAttributes.Create(Mode.GlobbedHref, new [] { HrefIncludeAttributeName, HrefExcludeAttributeName }),
|
||||
// Fallback with static href
|
||||
ModeAttributes.Create(
|
||||
Mode.Fallback, new[]
|
||||
{
|
||||
FallbackHrefAttributeName,
|
||||
FallbackTestClassAttributeName,
|
||||
FallbackTestPropertyAttributeName,
|
||||
FallbackTestValueAttributeName
|
||||
}),
|
||||
// Fallback with globbed href (include only)
|
||||
ModeAttributes.Create(
|
||||
Mode.Fallback, new[] {
|
||||
FallbackHrefIncludeAttributeName,
|
||||
FallbackTestClassAttributeName,
|
||||
FallbackTestPropertyAttributeName,
|
||||
FallbackTestValueAttributeName
|
||||
}),
|
||||
// Fallback with globbed href (include & exclude)
|
||||
ModeAttributes.Create(
|
||||
Mode.Fallback, new[] {
|
||||
FallbackHrefIncludeAttributeName,
|
||||
FallbackHrefExcludeAttributeName,
|
||||
FallbackTestClassAttributeName,
|
||||
FallbackTestPropertyAttributeName,
|
||||
FallbackTestValueAttributeName
|
||||
}),
|
||||
};
|
||||
|
||||
private enum Mode
|
||||
{
|
||||
/// <summary>
|
||||
/// Rendering a fallback block if primary stylesheet fails to load. Will also do globbing if the appropriate
|
||||
/// properties are set.
|
||||
/// </summary>
|
||||
Fallback,
|
||||
/// <summary>
|
||||
/// Just performing file globbing search for the href, rendering a separate <link> for each match.
|
||||
/// </summary>
|
||||
GlobbedHref
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A comma separated list of globbed file patterns of CSS stylesheets to load.
|
||||
/// The glob patterns are assessed relative to the application's 'webroot' setting.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(HrefIncludeAttributeName)]
|
||||
public string HrefInclude { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A comma separated list of globbed file patterns of CSS stylesheets to exclude from loading.
|
||||
/// The glob patterns are assessed relative to the application's 'webroot' setting.
|
||||
/// Must be used in conjunction with <see cref="HrefInclude"/>.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(HrefExcludeAttributeName)]
|
||||
public string HrefExclude { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL of a CSS stylesheet to fallback to in the case the primary one fails (as specified in the href
|
||||
/// attribute).
|
||||
|
|
@ -36,70 +99,171 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
[HtmlAttributeName(FallbackHrefAttributeName)]
|
||||
public string FallbackHref { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A comma separated list of globbed file patterns of CSS stylesheets to fallback to in the case the primary
|
||||
/// one fails (as specified in the href attribute).
|
||||
/// The glob patterns are assessed relative to the application's 'webroot' setting.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(FallbackHrefIncludeAttributeName)]
|
||||
public string FallbackHrefInclude { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A comma separated list of globbed file patterns of CSS stylesheets to exclude from the fallback list, in
|
||||
/// the case the primary one fails (as specified in the href attribute).
|
||||
/// The glob patterns are assessed relative to the application's 'webroot' setting.
|
||||
/// Must be used in conjunction with <see cref="FallbackHrefInclude"/>.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(FallbackHrefExcludeAttributeName)]
|
||||
public string FallbackHrefExclude { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The class name defined in the stylesheet to use for the fallback test.
|
||||
/// Must be used in conjunction with <see cref="FallbackTestProperty"/> and <see cref="FallbackTestValue"/>,
|
||||
/// and either <see cref="FallbackHref"/> or <see cref="FallbackHrefInclude"/>.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(FallbackTestClassAttributeName)]
|
||||
public string FallbackTestClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The CSS property name to use for the fallback test.
|
||||
/// Must be used in conjunction with <see cref="FallbackTestClass"/> and <see cref="FallbackTestValue"/>,
|
||||
/// and either <see cref="FallbackHref"/> or <see cref="FallbackHrefInclude"/>.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(FallbackTestPropertyAttributeName)]
|
||||
public string FallbackTestProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The CSS property value to use for the fallback test.
|
||||
/// Must be used in conjunction with <see cref="FallbackTestClass"/> and <see cref="FallbackTestProperty"/>,
|
||||
/// and either <see cref="FallbackHref"/> or <see cref="FallbackHrefInclude"/>.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(FallbackTestValueAttributeName)]
|
||||
public string FallbackTestValue { get; set; }
|
||||
|
||||
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
|
||||
// Properties are protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
|
||||
[Activate]
|
||||
protected internal ILogger<LinkTagHelper> Logger { get; set; }
|
||||
|
||||
[Activate]
|
||||
protected internal IHostingEnvironment HostingEnvironment { get; set; }
|
||||
|
||||
[Activate]
|
||||
protected internal ViewContext ViewContext { get; set; }
|
||||
|
||||
[Activate]
|
||||
protected internal IMemoryCache Cache { get; set; }
|
||||
|
||||
// Internal for ease of use when testing.
|
||||
protected internal GlobbingUrlBuilder GlobbingUrlBuilder { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
if (!context.AllRequiredAttributesArePresent(RequiredAttributes, Logger))
|
||||
var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails);
|
||||
|
||||
Debug.Assert(modeResult.FullMatches.Select(match => match.Mode).Distinct().Count() <= 1,
|
||||
$"There should only be one mode match, check the {nameof(ModeDetails)}");
|
||||
|
||||
modeResult.LogDetails(Logger, this, context.UniqueId);
|
||||
|
||||
if (!modeResult.FullMatches.Any())
|
||||
{
|
||||
if (Logger.IsEnabled(LogLevel.Verbose))
|
||||
{
|
||||
Logger.WriteVerbose("Skipping processing for {0} {1}", nameof(LinkTagHelper), context.UniqueId);
|
||||
}
|
||||
// No attributes matched so we have nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
var content = new StringBuilder();
|
||||
var mode = modeResult.FullMatches.First().Mode;
|
||||
|
||||
// NOTE: Values in TagHelperOutput.Attributes are already HtmlEncoded
|
||||
var attributes = new Dictionary<string, string>(output.Attributes);
|
||||
|
||||
// We've taken over rendering here so prevent the element rendering the outer tag
|
||||
output.TagName = null;
|
||||
var builder = new StringBuilder();
|
||||
|
||||
// Rebuild the <link /> tag that loads the primary stylesheet
|
||||
content.Append("<link ");
|
||||
foreach (var attribute in output.Attributes)
|
||||
if (mode == Mode.Fallback && string.IsNullOrEmpty(HrefInclude))
|
||||
{
|
||||
content.AppendFormat(CultureInfo.InvariantCulture, "{0}=\"{1}\" ", attribute.Key, attribute.Value);
|
||||
// No globbing to do, just build a <link /> tag to match the original one in the source file
|
||||
BuildLinkTag(attributes, builder);
|
||||
}
|
||||
else
|
||||
{
|
||||
BuildGlobbedLinkTags(attributes, builder);
|
||||
}
|
||||
content.AppendLine("/>");
|
||||
|
||||
// Build the <meta /> tag that's used to test for the presence of the stylesheet
|
||||
content.AppendLine(string.Format(CultureInfo.InvariantCulture, FallbackTestMetaTemplate, FallbackTestClass));
|
||||
if (mode == Mode.Fallback)
|
||||
{
|
||||
BuildFallbackBlock(builder);
|
||||
}
|
||||
|
||||
// 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.
|
||||
content.Append("<script>");
|
||||
content.AppendFormat(CultureInfo.InvariantCulture,
|
||||
JavaScriptUtility.GetEmbeddedJavaScript(FallbackJavaScriptResourceName),
|
||||
JavaScriptUtility.JavaScriptStringEncode(FallbackTestProperty),
|
||||
JavaScriptUtility.JavaScriptStringEncode(FallbackTestValue),
|
||||
JavaScriptUtility.JavaScriptStringEncode(FallbackHref));
|
||||
content.Append("</script>");
|
||||
// We've taken over tag rendering, so prevent rendering the outer tag
|
||||
output.TagName = null;
|
||||
output.Content = builder.ToString();
|
||||
}
|
||||
|
||||
output.Content = content.ToString();
|
||||
private void BuildGlobbedLinkTags(IDictionary<string, string> attributes, StringBuilder builder)
|
||||
{
|
||||
// Build a <link /> tag for each matched href as well as the original one in the source file
|
||||
string staticHref;
|
||||
attributes.TryGetValue("href", out staticHref);
|
||||
|
||||
EnsureGlobbingUrlBuilder();
|
||||
var hrefs = GlobbingUrlBuilder.BuildUrlList(staticHref, HrefInclude, HrefExclude);
|
||||
|
||||
foreach (var href in hrefs)
|
||||
{
|
||||
attributes["href"] = WebUtility.HtmlEncode(href);
|
||||
BuildLinkTag(attributes, builder);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildFallbackBlock(StringBuilder builder)
|
||||
{
|
||||
EnsureGlobbingUrlBuilder();
|
||||
var fallbackHrefs = GlobbingUrlBuilder.BuildUrlList(FallbackHref, FallbackHrefInclude, FallbackHrefExclude);
|
||||
|
||||
if (fallbackHrefs.Any())
|
||||
{
|
||||
builder.AppendLine();
|
||||
|
||||
// Build the <meta /> tag that's used to test for the presence of the stylesheet
|
||||
builder.AppendFormat(
|
||||
CultureInfo.InvariantCulture,
|
||||
"<meta name=\"x-stylesheet-fallback-test\" class=\"{0}\" />",
|
||||
WebUtility.HtmlEncode(FallbackTestClass));
|
||||
|
||||
// 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.
|
||||
builder.Append("<script>")
|
||||
.AppendFormat(CultureInfo.InvariantCulture,
|
||||
JavaScriptResources.GetEmbeddedJavaScript(FallbackJavaScriptResourceName),
|
||||
JavaScriptStringEncoder.Default.JavaScriptStringEncode(FallbackTestProperty),
|
||||
JavaScriptStringEncoder.Default.JavaScriptStringEncode(FallbackTestValue),
|
||||
JavaScriptStringArrayEncoder.Encode(JavaScriptStringEncoder.Default, fallbackHrefs))
|
||||
.Append("</script>");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureGlobbingUrlBuilder()
|
||||
{
|
||||
if (GlobbingUrlBuilder == null)
|
||||
{
|
||||
GlobbingUrlBuilder = new GlobbingUrlBuilder(
|
||||
HostingEnvironment.WebRootFileProvider,
|
||||
Cache,
|
||||
ViewContext.HttpContext.Request.PathBase);
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildLinkTag(IDictionary<string, string> attributes, StringBuilder builder)
|
||||
{
|
||||
builder.Append("<link ");
|
||||
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "{0}=\"{1}\" ", attribute.Key, attribute.Value);
|
||||
}
|
||||
|
||||
builder.Append("/>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,3 +4,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.TagHelpers.Test")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ using System.Globalization;
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.AspNet.WebUtilities.Encoders;
|
||||
using Microsoft.Framework.Logging;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
|
|
@ -51,7 +53,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
/// <inheritdoc />
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
if (!context.AllRequiredAttributesArePresent(RequiredAttributes, Logger))
|
||||
if (!AttributeMatcher.AllRequiredAttributesArePresent(context, RequiredAttributes, Logger))
|
||||
{
|
||||
if (Logger.IsEnabled(LogLevel.Verbose))
|
||||
{
|
||||
|
|
@ -95,8 +97,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
{
|
||||
if (!attribute.Key.Equals(SrcAttributeName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var encodedKey = JavaScriptUtility.JavaScriptStringEncode(attribute.Key);
|
||||
var encodedValue = JavaScriptUtility.JavaScriptStringEncode(attribute.Value);
|
||||
var encodedKey = JavaScriptStringEncoder.Default.JavaScriptStringEncode(attribute.Key);
|
||||
var encodedValue = JavaScriptStringEncoder.Default.JavaScriptStringEncode(attribute.Value);
|
||||
|
||||
content.AppendFormat(CultureInfo.InvariantCulture, " {0}=\\\"{1}\\\"", encodedKey, encodedValue);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// 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.Linq;
|
||||
using Microsoft.Framework.Logging;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
using Microsoft.AspNet.Mvc.TagHelpers;
|
||||
|
||||
namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility related extensions for <see cref="TagHelperContext"/>.
|
||||
/// </summary>
|
||||
public static class TagHelperContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether a <see cref="ITagHelper" />'s required attributes are present, non null, non empty, and
|
||||
/// non whitepsace.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="TagHelperContext"/>.</param>
|
||||
/// <param name="requiredAttributes">The attributes the <see cref="ITagHelper" /> requires in order to run.</param>
|
||||
/// <param name="logger">An optional <see cref="ILogger"/> to log warning details to.</param>
|
||||
/// <returns>A <see cref="bool"/> indicating whether the <see cref="ITagHelper" /> should run.</returns>
|
||||
public static bool AllRequiredAttributesArePresent(
|
||||
[NotNull]this TagHelperContext context,
|
||||
[NotNull]IEnumerable<string> requiredAttributes,
|
||||
ILogger logger = null)
|
||||
{
|
||||
// Check for all attribute values & log a warning if any required are missing
|
||||
var atLeastOnePresent = false;
|
||||
var missingAttrNames = new List<string>();
|
||||
|
||||
foreach (var attr in requiredAttributes)
|
||||
{
|
||||
if (!context.AllAttributes.ContainsKey(attr)
|
||||
|| context.AllAttributes[attr] == null
|
||||
|| string.IsNullOrWhiteSpace(context.AllAttributes[attr] as string))
|
||||
{
|
||||
// Missing attribute!
|
||||
missingAttrNames.Add(attr);
|
||||
}
|
||||
else
|
||||
{
|
||||
atLeastOnePresent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (missingAttrNames.Any())
|
||||
{
|
||||
if (atLeastOnePresent && logger != null && logger.IsEnabled(LogLevel.Warning))
|
||||
{
|
||||
// At least 1 attribute was present indicating the user intended to use the tag helper,
|
||||
// but at least 1 was missing too, so log a warning with the details.
|
||||
logger.WriteWarning(new MissingAttributeLoggerStructure(context.UniqueId, missingAttrNames));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// All required attributes present
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,26 @@
|
|||
(function (cssTestPropertyName, cssTestPropertyValue, fallbackHref) {
|
||||
// This function finds the previous element (assumed to be meta) and tests its current CSS style using the passed
|
||||
// values, to determine if a stylesheet was loaded. If not, this function loads the fallback stylesheet via
|
||||
// document.write.
|
||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
(
|
||||
/**
|
||||
* This function finds the previous element (assumed to be meta) and tests its current CSS style using the passed
|
||||
* values, to determine if a stylesheet was loaded. If not, this function loads the fallback stylesheet via
|
||||
* document.write.
|
||||
*
|
||||
* @param {string} cssTestPropertyName - The name of the CSS property to test.
|
||||
* @param {string} cssTestPropertyValue - The value to test the specified CSS property for.
|
||||
* @param {string[]} fallbackHref - The URLs to the stylesheets to load in the case the test fails.
|
||||
*/
|
||||
function loadFallbackStylesheet(cssTestPropertyName, cssTestPropertyValue, fallbackHref) {
|
||||
var doc = document,
|
||||
// Find the last script tag on the page which will be this one, as JS executes as it loads
|
||||
scriptElements = doc.getElementsByTagName("SCRIPT"),
|
||||
meta = scriptElements[scriptElements.length - 1].previousElementSibling;
|
||||
// Find the meta tag before this script tag, that's the element we're going to test the CSS property on
|
||||
meta = scriptElements[scriptElements.length - 1].previousElementSibling,
|
||||
i;
|
||||
|
||||
if (doc.defaultView.getComputedStyle(meta)[cssTestPropertyName] !== cssTestPropertyValue) {
|
||||
doc.write('<link rel="stylesheet" href="' + fallbackHref + '"/>');
|
||||
for (i = 0; i < fallbackHref.length; i++) {
|
||||
doc.write('<link rel="stylesheet" href="' + fallbackHref[i] + '"/>');
|
||||
}
|
||||
}
|
||||
})("[[[0]]]", "[[[1]]]", "[[[2]]]");
|
||||
})("[[[0]]]", "[[[1]]]", [[[2]]]);
|
||||
|
|
@ -7,7 +7,9 @@
|
|||
"dependencies": {
|
||||
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
|
||||
"Microsoft.AspNet.Mvc.Razor": "6.0.0-*",
|
||||
"Microsoft.Framework.Logging.Interfaces": "1.0.0-*",
|
||||
"Microsoft.Framework.Cache.Memory": "1.0.0-*",
|
||||
"Microsoft.Framework.FileSystemGlobbing": "1.0.0-*",
|
||||
"Microsoft.Framework.Logging.Interfaces": { "version": "1.0.0-*", "type": "build" },
|
||||
"System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*"
|
||||
},
|
||||
"frameworks": {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,75 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Link</title>
|
||||
<link href="/blank.css" rel="stylesheet"></link>
|
||||
<link rel="stylesheet" href="/link-test.min.css" data-extra="test" />
|
||||
<meta name="x-stylesheet-fallback-test" class="hidden" />
|
||||
<script>!function(a,b,c){var d=document,e=d.getElementsByTagName("SCRIPT"),f=e[e.length-1].previousElementSibling;d.defaultView.getComputedStyle(f)[a]!==b&&d.write('<link rel="stylesheet" href="'+c+'"/>')}("visibility","hidden","/link-test.css");</script>
|
||||
|
||||
<!-- Plain link tag -->
|
||||
<link href="/site.css" rel="stylesheet"></link>
|
||||
|
||||
<!-- Globbed link tag with existing file -->
|
||||
<link rel="stylesheet" href="/site.css" />
|
||||
|
||||
<!-- Globbed link tag with existing file and exclude -->
|
||||
<link rel="stylesheet" href="/site.css" /><link rel="stylesheet" href="/sub/site2.css" />
|
||||
|
||||
<!-- Globbed link tag missing include -->
|
||||
<link rel="stylesheet"></link>
|
||||
|
||||
<!-- Globbed link tag missing include but with static href -->
|
||||
<link href="/site.css" rel="stylesheet"></link>
|
||||
|
||||
<!-- Globbed link tag with missing file -->
|
||||
|
||||
|
||||
<!-- Globbed link tag with file outside of webroot -->
|
||||
|
||||
|
||||
<!-- Globbed link tag with file outside of webroot -->
|
||||
|
||||
|
||||
<!-- Globbed link tag with existing file and static href -->
|
||||
<link href="/site.css" rel="stylesheet" /><link href="/sub/site2.css" rel="stylesheet" />
|
||||
|
||||
<!-- Globbed link tag with existing file and static href should dedupe -->
|
||||
<link href="/site.css" rel="stylesheet" />
|
||||
|
||||
<!-- Fallback to static href -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test" />
|
||||
<meta name="x-stylesheet-fallback-test" class="hidden" /><script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName("SCRIPT"),g=f[f.length-1].previousElementSibling;if(e.defaultView.getComputedStyle(g)[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}("visibility","hidden",["\/site.css"]);</script>
|
||||
|
||||
<!-- Fallback to static href with no primary href -->
|
||||
<link rel="stylesheet" data-extra="test" />
|
||||
<meta name="x-stylesheet-fallback-test" class="hidden" /><script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName("SCRIPT"),g=f[f.length-1].previousElementSibling;if(e.defaultView.getComputedStyle(g)[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}("visibility","hidden",["\/site.css"]);</script>
|
||||
|
||||
<!-- Fallback to globbed href -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test" />
|
||||
<meta name="x-stylesheet-fallback-test" class="hidden" /><script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName("SCRIPT"),g=f[f.length-1].previousElementSibling;if(e.defaultView.getComputedStyle(g)[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}("visibility","hidden",["\/site.css"]);</script>
|
||||
|
||||
<!-- Fallback to static and globbed href -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test" />
|
||||
<meta name="x-stylesheet-fallback-test" class="hidden" /><script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName("SCRIPT"),g=f[f.length-1].previousElementSibling;if(e.defaultView.getComputedStyle(g)[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}("visibility","hidden",["\/site.css","\/sub\/site2.css"]);</script>
|
||||
|
||||
<!-- Fallback to static and globbed href should dedupe -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test" />
|
||||
<meta name="x-stylesheet-fallback-test" class="hidden" /><script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName("SCRIPT"),g=f[f.length-1].previousElementSibling;if(e.defaultView.getComputedStyle(g)[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}("visibility","hidden",["\/site.css"]);</script>
|
||||
|
||||
<!-- Fallback to static and globbed href with exclude -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test" />
|
||||
<meta name="x-stylesheet-fallback-test" class="hidden" /><script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName("SCRIPT"),g=f[f.length-1].previousElementSibling;if(e.defaultView.getComputedStyle(g)[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}("visibility","hidden",["\/site.css","\/sub\/site2.css"]);</script>
|
||||
|
||||
<!-- Fallback to globbed href that doesn't exist -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test" />
|
||||
|
||||
<!-- Fallback to globbed href outside webroot -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test" />
|
||||
|
||||
<!-- Fallback with missing attribute -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test"></link>
|
||||
|
||||
<!-- Fallback with missing attribute -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test"></link>
|
||||
|
||||
<!-- Fallback with missing attribute -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test"></link>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
public class AttributeMatcherTest
|
||||
{
|
||||
[Fact]
|
||||
public void DetermineMode_FindsFullModeMatchWithSingleAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var modeInfo = new []
|
||||
{
|
||||
ModeAttributes.Create("mode0", new [] { "first-attr" })
|
||||
};
|
||||
var attributes = new Dictionary<string, object>
|
||||
{
|
||||
["first-attr"] = "value",
|
||||
["not-in-any-mode"] = "value"
|
||||
};
|
||||
var context = MakeTagHelperContext(attributes);
|
||||
|
||||
// Act
|
||||
var modeMatch = AttributeMatcher.DetermineMode(context, modeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(modeMatch.FullMatches, match =>
|
||||
{
|
||||
Assert.Equal("mode0", match.Mode);
|
||||
Assert.Collection(match.PresentAttributes, attribute => Assert.Equal("first-attr", attribute));
|
||||
});
|
||||
Assert.Empty(modeMatch.PartialMatches);
|
||||
Assert.Empty(modeMatch.PartiallyMatchedAttributes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineMode_FindsFullModeMatchWithMultipleAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var modeInfo = new[]
|
||||
{
|
||||
ModeAttributes.Create("mode0", new [] { "first-attr", "second-attr" })
|
||||
};
|
||||
var attributes = new Dictionary<string, object>
|
||||
{
|
||||
["first-attr"] = "value",
|
||||
["second-attr"] = "value",
|
||||
["not-in-any-mode"] = "value"
|
||||
};
|
||||
var context = MakeTagHelperContext(attributes);
|
||||
|
||||
// Act
|
||||
var modeMatch = AttributeMatcher.DetermineMode(context, modeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(modeMatch.FullMatches, match =>
|
||||
{
|
||||
Assert.Equal("mode0", match.Mode);
|
||||
Assert.Collection(match.PresentAttributes,
|
||||
attribute => Assert.Equal("first-attr", attribute),
|
||||
attribute => Assert.Equal("second-attr", attribute)
|
||||
);
|
||||
});
|
||||
Assert.Empty(modeMatch.PartialMatches);
|
||||
Assert.Empty(modeMatch.PartiallyMatchedAttributes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineMode_FindsFullAndPartialModeMatchWithMultipleAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var modeInfo = new[]
|
||||
{
|
||||
ModeAttributes.Create("mode0", new [] { "second-attr" }),
|
||||
ModeAttributes.Create("mode1", new [] { "first-attr", "third-attr" }),
|
||||
ModeAttributes.Create("mode2", new [] { "first-attr", "second-attr", "third-attr" }),
|
||||
ModeAttributes.Create("mode3", new [] { "fourth-attr" })
|
||||
};
|
||||
var attributes = new Dictionary<string, object>
|
||||
{
|
||||
["second-attr"] = "value",
|
||||
["third-attr"] = "value",
|
||||
["not-in-any-mode"] = "value"
|
||||
};
|
||||
var context = MakeTagHelperContext(attributes);
|
||||
|
||||
// Act
|
||||
var modeMatch = AttributeMatcher.DetermineMode(context, modeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(modeMatch.FullMatches, match =>
|
||||
{
|
||||
Assert.Equal("mode0", match.Mode);
|
||||
Assert.Collection(match.PresentAttributes, attribute => Assert.Equal("second-attr", attribute));
|
||||
});
|
||||
Assert.Collection(modeMatch.PartialMatches,
|
||||
match =>
|
||||
{
|
||||
Assert.Equal("mode1", match.Mode);
|
||||
Assert.Collection(match.PresentAttributes, attribute => Assert.Equal("third-attr", attribute));
|
||||
Assert.Collection(match.MissingAttributes, attribute => Assert.Equal("first-attr", attribute));
|
||||
},
|
||||
match =>
|
||||
{
|
||||
Assert.Equal("mode2", match.Mode);
|
||||
Assert.Collection(match.PresentAttributes,
|
||||
attribute => Assert.Equal("second-attr", attribute),
|
||||
attribute => Assert.Equal("third-attr", attribute)
|
||||
);
|
||||
Assert.Collection(match.MissingAttributes, attribute => Assert.Equal("first-attr", attribute));
|
||||
});
|
||||
Assert.Collection(modeMatch.PartiallyMatchedAttributes, attribute => Assert.Equal("third-attr", attribute));
|
||||
}
|
||||
|
||||
private static TagHelperContext MakeTagHelperContext(
|
||||
IDictionary<string, object> attributes = null,
|
||||
string content = null)
|
||||
{
|
||||
attributes = attributes ?? new Dictionary<string, object>();
|
||||
|
||||
return new TagHelperContext(attributes, Guid.NewGuid().ToString("N"), () => Task.FromResult(content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// 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.Linq;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.Framework.Cache.Memory;
|
||||
using Microsoft.Framework.Expiration.Interfaces;
|
||||
using Microsoft.Framework.FileSystemGlobbing;
|
||||
using Microsoft.Framework.FileSystemGlobbing.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
public class GlobbingUrlBuilderTest
|
||||
{
|
||||
[Fact]
|
||||
public void ReturnsOnlyStaticUrlWhenPatternDoesntFindAnyMatches()
|
||||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider();
|
||||
IMemoryCache cache = null;
|
||||
var requestPathBase = PathString.Empty;
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
// Act
|
||||
var urlList = globbingUrlBuilder.BuildUrlList("/site.css", "**/*.css", excludePattern: null);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(urlList, url => Assert.Equal("/site.css", url));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DedupesStaticUrlAndPatternMatches()
|
||||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
|
||||
IMemoryCache cache = null;
|
||||
var requestPathBase = PathString.Empty;
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
// Act
|
||||
var urlList = globbingUrlBuilder.BuildUrlList("/site.css", "**/*.css", excludePattern: null);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(urlList,
|
||||
url => Assert.Equal("/site.css", url),
|
||||
url => Assert.Equal("/blank.css", url));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/sub")]
|
||||
[InlineData("/sub/again")]
|
||||
public void ResolvesMatchedUrlsAgainstPathBase(string pathBase)
|
||||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
|
||||
IMemoryCache cache = null;
|
||||
var requestPathBase = new PathString(pathBase);
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
// Act
|
||||
var urlList = globbingUrlBuilder.BuildUrlList(
|
||||
staticUrl: null,
|
||||
includePattern: "**/*.css",
|
||||
excludePattern: null);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(urlList,
|
||||
url => Assert.Equal($"{pathBase}/site.css", url),
|
||||
url => Assert.Equal($"{pathBase}/blank.css", url));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsesCachedMatchResults()
|
||||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider();
|
||||
var cache = MakeCache(new List<string> { "/site.css", "/blank.css" });
|
||||
var requestPathBase = PathString.Empty;
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
// Act
|
||||
var urlList = globbingUrlBuilder.BuildUrlList(
|
||||
staticUrl: null,
|
||||
includePattern: "**/*.css",
|
||||
excludePattern: null);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(urlList,
|
||||
url => Assert.Equal("/site.css", url),
|
||||
url => Assert.Equal("/blank.css", url));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CachesMatchResults()
|
||||
{
|
||||
// Arrange
|
||||
var trigger = new Mock<IExpirationTrigger>();
|
||||
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
|
||||
Mock.Get(fileProvider).Setup(f => f.Watch(It.IsAny<string>())).Returns(trigger.Object);
|
||||
var cache = MakeCache();
|
||||
var cacheSetContext = new Mock<ICacheSetContext>();
|
||||
cacheSetContext.Setup(c => c.AddExpirationTrigger(trigger.Object)).Verifiable();
|
||||
Mock.Get(cache).Setup(c => c.Set(
|
||||
/*key*/ It.IsAny<string>(),
|
||||
/*link*/ It.IsAny<IEntryLink>(),
|
||||
/*state*/ It.IsAny<object>(),
|
||||
/*create*/ It.IsAny<Func<ICacheSetContext, object>>()))
|
||||
.Returns<string, IEntryLink, object, Func<ICacheSetContext, object>>(
|
||||
(key, link, state, create) =>
|
||||
{
|
||||
cacheSetContext.Setup(c => c.State).Returns(state);
|
||||
return create(cacheSetContext.Object);
|
||||
})
|
||||
.Verifiable();
|
||||
var requestPathBase = PathString.Empty;
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
|
||||
// Act
|
||||
var urlList = globbingUrlBuilder.BuildUrlList(
|
||||
staticUrl: null,
|
||||
includePattern: "**/*.css",
|
||||
excludePattern: null);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(urlList,
|
||||
url => Assert.Equal("/site.css", url),
|
||||
url => Assert.Equal("/blank.css", url));
|
||||
cacheSetContext.VerifyAll();
|
||||
Mock.Get(cache).VerifyAll();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/")]
|
||||
[InlineData("\\")]
|
||||
public void TrimsLeadingSlashFromPatterns(string leadingSlash)
|
||||
{
|
||||
// Arrange
|
||||
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
|
||||
IMemoryCache cache = null;
|
||||
var requestPathBase = PathString.Empty;
|
||||
var includePatterns = new List<string>();
|
||||
var excludePatterns = new List<string>();
|
||||
var matcher = MakeMatcher(includePatterns, excludePatterns);
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
globbingUrlBuilder.MatcherBuilder = () => matcher;
|
||||
|
||||
// Act
|
||||
var urlList = globbingUrlBuilder.BuildUrlList(
|
||||
staticUrl: null,
|
||||
includePattern: $"{leadingSlash}**/*.css",
|
||||
excludePattern: $"{leadingSlash}**/*.min.css");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(includePatterns, pattern => Assert.Equal("**/*.css", pattern));
|
||||
Assert.Collection(excludePatterns, pattern => Assert.Equal("**/*.min.css", pattern));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/")]
|
||||
[InlineData("\\")]
|
||||
public void TrimsOnlySingleLeadingSlashFromPatterns(string leadingSlash)
|
||||
{
|
||||
// Arrange
|
||||
var leadingSlashes = $"{leadingSlash}{leadingSlash}";
|
||||
var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
|
||||
IMemoryCache cache = null;
|
||||
var requestPathBase = PathString.Empty;
|
||||
var includePatterns = new List<string>();
|
||||
var excludePatterns = new List<string>();
|
||||
var matcher = MakeMatcher(includePatterns, excludePatterns);
|
||||
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
|
||||
globbingUrlBuilder.MatcherBuilder = () => matcher;
|
||||
|
||||
// Act
|
||||
var urlList = globbingUrlBuilder.BuildUrlList(
|
||||
staticUrl: null,
|
||||
includePattern: $"{leadingSlashes}**/*.css",
|
||||
excludePattern: $"{leadingSlashes}**/*.min.css");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(includePatterns, pattern => Assert.Equal($"{leadingSlash}**/*.css", pattern));
|
||||
Assert.Collection(excludePatterns, pattern => Assert.Equal($"{leadingSlash}**/*.min.css", pattern));
|
||||
}
|
||||
|
||||
private static IFileInfo MakeFileInfo(string name)
|
||||
{
|
||||
var fileInfo = new Mock<IFileInfo>();
|
||||
fileInfo.Setup(f => f.Name).Returns(name);
|
||||
return fileInfo.Object;
|
||||
}
|
||||
|
||||
private static IDirectoryContents MakeDirectoryContents(params string[] fileNames)
|
||||
{
|
||||
var files = fileNames.Select(name => MakeFileInfo(name));
|
||||
var directoryContents = new Mock<IDirectoryContents>();
|
||||
directoryContents.Setup(dc => dc.GetEnumerator()).Returns(files.GetEnumerator());
|
||||
|
||||
return directoryContents.Object;
|
||||
}
|
||||
|
||||
private static IFileProvider MakeFileProvider(IDirectoryContents directoryContents = null)
|
||||
{
|
||||
if (directoryContents == null)
|
||||
{
|
||||
directoryContents = MakeDirectoryContents();
|
||||
}
|
||||
|
||||
var fileProvider = new Mock<IFileProvider>();
|
||||
fileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny<string>()))
|
||||
.Returns(directoryContents);
|
||||
return fileProvider.Object;
|
||||
}
|
||||
|
||||
private static IMemoryCache MakeCache(object result = null)
|
||||
{
|
||||
var cache = new Mock<IMemoryCache>();
|
||||
cache.Setup(c => c.TryGetValue(It.IsAny<string>(), It.IsAny<IEntryLink>(), out result))
|
||||
.Returns(result != null);
|
||||
return cache.Object;
|
||||
}
|
||||
|
||||
private static Matcher MakeMatcher(List<string> includePatterns, List<string> excludePatterns)
|
||||
{
|
||||
var matcher = new Mock<Matcher>();
|
||||
matcher.Setup(m => m.AddInclude(It.IsAny<string>()))
|
||||
.Returns<string>(pattern =>
|
||||
{
|
||||
includePatterns.Add(pattern);
|
||||
return matcher.Object;
|
||||
});
|
||||
matcher.Setup(m => m.AddExclude(It.IsAny<string>()))
|
||||
.Returns<string>(pattern =>
|
||||
{
|
||||
excludePatterns.Add(pattern);
|
||||
return matcher.Object;
|
||||
});
|
||||
var patternMatchingResult = new PatternMatchingResult(Enumerable.Empty<string>());
|
||||
matcher.Setup(m => m.Execute(It.IsAny<DirectoryInfoBase>())).Returns(patternMatchingResult);
|
||||
return matcher.Object;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
public class JavaScriptResourcesTest
|
||||
{
|
||||
[Fact]
|
||||
public void GetEmbeddedJavaScript_LoadsEmbeddedResourceFromManifestStream()
|
||||
{
|
||||
// Arrange
|
||||
var resource = "window.alert('An alert');";
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(resource));
|
||||
var getManifestResourceStream = new Func<string, Stream>(name => stream);
|
||||
var cache = new ConcurrentDictionary<string, string>();
|
||||
|
||||
// Act
|
||||
var result = JavaScriptResources.GetEmbeddedJavaScript("test.js", getManifestResourceStream, cache);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(resource, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEmbeddedJavaScript_AddsResourceToCacheWhenRead()
|
||||
{
|
||||
// Arrange
|
||||
var resource = "window.alert('An alert');";
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(resource));
|
||||
var getManifestResourceStream = new Func<string, Stream>(name => stream);
|
||||
var cache = new ConcurrentDictionary<string, string>();
|
||||
|
||||
// Act
|
||||
var result = JavaScriptResources.GetEmbeddedJavaScript("test.js", getManifestResourceStream, cache);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(cache, kvp =>
|
||||
{
|
||||
Assert.Equal("test.js", kvp.Key);
|
||||
Assert.Equal(resource, kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEmbeddedJavaScript_LoadsResourceFromCacheAfterInitialCall()
|
||||
{
|
||||
// Arrange
|
||||
var resource = "window.alert('An alert');";
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(resource));
|
||||
var callCount = 0;
|
||||
var getManifestResourceStream = new Func<string, Stream>(name =>
|
||||
{
|
||||
callCount++;
|
||||
return stream;
|
||||
});
|
||||
var cache = new ConcurrentDictionary<string, string>();
|
||||
|
||||
// Act
|
||||
var result = JavaScriptResources.GetEmbeddedJavaScript("test.js", getManifestResourceStream, cache);
|
||||
result = JavaScriptResources.GetEmbeddedJavaScript("test.js", getManifestResourceStream, cache);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, callCount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("window.alert(\"[[[0]]]\")", "window.alert(\"{0}\")")]
|
||||
[InlineData("var test = { a: 1 };", "var test = {{ a: 1 }};")]
|
||||
[InlineData("var test = { a: 1, b: \"[[[0]]]\" };", "var test = {{ a: 1, b: \"{0}\" }};")]
|
||||
public void GetEmbeddedJavaScript_PreparesJavaScriptCorrectly(string resource, string expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(resource));
|
||||
var getManifestResourceStream = new Func<string, Stream>(name => stream);
|
||||
var cache = new ConcurrentDictionary<string, string>();
|
||||
|
||||
// Act
|
||||
var result = JavaScriptResources.GetEmbeddedJavaScript("test.js", getManifestResourceStream, cache);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.Framework.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
public class ModeMatchResultTest
|
||||
{
|
||||
[Fact]
|
||||
public void LogDetails_LogsVerboseWhenNoFullMatchesFound()
|
||||
{
|
||||
// Arrange
|
||||
var modeMatchResult = new ModeMatchResult<string>();
|
||||
var logger = MakeLogger(LogLevel.Verbose);
|
||||
var tagHelper = new Mock<ITagHelper>();
|
||||
var uniqueId = "id";
|
||||
|
||||
// Act
|
||||
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId);
|
||||
|
||||
// Assert
|
||||
Mock.Get(logger).Verify(l => l.Write(
|
||||
LogLevel.Verbose,
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<object, Exception, string>>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogDetails_DoesNotLogWhenPartialMatchFoundButNoPartiallyMatchedAttributesFound()
|
||||
{
|
||||
// Arrange
|
||||
var modeMatchResult = new ModeMatchResult<string>();
|
||||
modeMatchResult.FullMatches.Add(
|
||||
ModeMatchAttributes.Create("mode0", new[] { "first-attr" }));
|
||||
modeMatchResult.PartialMatches.Add(
|
||||
ModeMatchAttributes.Create("mode1", new[] { "first-attr" }, new[] { "second-attr" }));
|
||||
var logger = MakeLogger(LogLevel.Verbose);
|
||||
var tagHelper = new Mock<ITagHelper>();
|
||||
var uniqueId = "id";
|
||||
|
||||
// Act
|
||||
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId);
|
||||
|
||||
// Assert
|
||||
Mock.Get(logger).Verify(l => l.Write(
|
||||
LogLevel.Warning,
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<object, Exception, string>>()), Times.Never);
|
||||
Mock.Get(logger).Verify(l => l.Write(
|
||||
LogLevel.Verbose,
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<object, Exception, string>>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogDetails_LogsWhenPartiallyMatchedAttributesFound()
|
||||
{
|
||||
// Arrange
|
||||
var modeMatchResult = new ModeMatchResult<string>();
|
||||
modeMatchResult.PartialMatches.Add(
|
||||
ModeMatchAttributes.Create("mode0", new[] { "first-attr" }, new[] { "second-attr" }));
|
||||
modeMatchResult.PartiallyMatchedAttributes.Add("first-attr");
|
||||
var logger = MakeLogger(LogLevel.Verbose);
|
||||
var tagHelper = new Mock<ITagHelper>();
|
||||
var uniqueId = "id";
|
||||
|
||||
// Act
|
||||
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId);
|
||||
|
||||
// Assert
|
||||
Mock.Get(logger).Verify(l => l.Write(
|
||||
LogLevel.Warning,
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<object, Exception, string>>()), Times.Once);
|
||||
Mock.Get(logger).Verify(l => l.Write(
|
||||
LogLevel.Verbose,
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<object, Exception, string>>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogDetails_DoesNotLogWhenLoggingLevelIsSetAboveWarning()
|
||||
{
|
||||
// Arrange
|
||||
var modeMatchResult = new ModeMatchResult<string>();
|
||||
modeMatchResult.PartialMatches.Add(
|
||||
ModeMatchAttributes.Create("mode0", new[] { "first-attr" }, new[] { "second-attr" }));
|
||||
modeMatchResult.PartiallyMatchedAttributes.Add("first-attr");
|
||||
var logger = MakeLogger(LogLevel.Critical);
|
||||
var tagHelper = new Mock<ITagHelper>();
|
||||
var uniqueId = "id";
|
||||
|
||||
// Act
|
||||
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId);
|
||||
|
||||
// Assert
|
||||
Mock.Get(logger).Verify(l => l.Write(
|
||||
LogLevel.Warning,
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<object, Exception, string>>()), Times.Never);
|
||||
Mock.Get(logger).Verify(l => l.Write(
|
||||
LogLevel.Verbose,
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<object, Exception, string>>()), Times.Never);
|
||||
}
|
||||
|
||||
private static ILogger MakeLogger(LogLevel level)
|
||||
{
|
||||
var logger = new Mock<ILogger>();
|
||||
logger.Setup(l => l.IsEnabled(It.IsAny<LogLevel>())).Returns<LogLevel>(l => l >= level);
|
||||
|
||||
return logger.Object;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Test
|
||||
{
|
||||
public class JavaScriptUtilityTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Hello World", "Hello World")]
|
||||
[InlineData("Hello & World", "Hello \\u0026 World")]
|
||||
[InlineData("Hello \r World", "Hello \\r World")]
|
||||
[InlineData("Hello \n World", "Hello \\n World")]
|
||||
[InlineData("Hello < World", "Hello \\u003c World")]
|
||||
[InlineData("Hello > World", "Hello \\u003e World")]
|
||||
[InlineData("Hello ' World", "Hello \\u0027 World")]
|
||||
[InlineData("Hello \" World", "Hello \\u0022 World")]
|
||||
[InlineData("Hello \\ World", "Hello \\\\ World")]
|
||||
[InlineData("Hello \u0005 \u001f World", "Hello \\u0005 \\u001f World")]
|
||||
[InlineData("Hello \r\n <ah /> 'eep' & \"hey\" World", "Hello \\r\\n \\u003cah /\\u003e \\u0027eep\\u0027 \\u0026 \\u0022hey\\u0022 World")]
|
||||
public void JavaScriptEncode_EncodesCorrectly(string input, string expectedOutput)
|
||||
{
|
||||
// Act
|
||||
var result = JavaScriptUtility.JavaScriptStringEncode(input);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedOutput, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("window.alert(\"[[[0]]]\")", "window.alert(\"{0}\")")]
|
||||
[InlineData("var test = { a: 1 };", "var test = {{ a: 1 }};")]
|
||||
[InlineData("var test = { a: 1, b: \"[[[0]]]\" };", "var test = {{ a: 1, b: \"{0}\" }};")]
|
||||
public void PrepareFormatString_PreparesJavaScriptCorrectly(string input, string expectedOutput)
|
||||
{
|
||||
// Act
|
||||
var result = JavaScriptUtility.PrepareFormatString(input);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedOutput, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,17 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.AspNet.Hosting;
|
||||
using Microsoft.AspNet.Http.Core;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.Framework.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
|
@ -13,30 +22,91 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
{
|
||||
public class LinkTagHelperTest
|
||||
{
|
||||
[Fact]
|
||||
public void RunsWhenRequiredAttributesArePresent()
|
||||
public static TheoryData RunsWhenRequiredAttributesArePresent_Data
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<IDictionary<string, object>, Action<LinkTagHelper>>
|
||||
{
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-href-include"] = "*.css"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.HrefInclude = "*.css";
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-href-include"] = "*.css",
|
||||
["asp-href-exclude"] = "*.min.css"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.HrefInclude = "*.css";
|
||||
tagHelper.HrefExclude = "*.min.css";
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-fallback-href"] = "test.css",
|
||||
["asp-fallback-test-class"] = "hidden",
|
||||
["asp-fallback-test-property"] = "visibility",
|
||||
["asp-fallback-test-value"] = "hidden"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.FallbackHref = "test.css";
|
||||
tagHelper.FallbackTestClass = "hidden";
|
||||
tagHelper.FallbackTestProperty = "visibility";
|
||||
tagHelper.FallbackTestValue = "hidden";
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-fallback-href-include"] = "*.css",
|
||||
["asp-fallback-test-class"] = "hidden",
|
||||
["asp-fallback-test-property"] = "visibility",
|
||||
["asp-fallback-test-value"] = "hidden"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.FallbackHrefInclude = "*.css";
|
||||
tagHelper.FallbackTestClass = "hidden";
|
||||
tagHelper.FallbackTestProperty = "visibility";
|
||||
tagHelper.FallbackTestValue = "hidden";
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RunsWhenRequiredAttributesArePresent_Data))]
|
||||
public void RunsWhenRequiredAttributesArePresent(
|
||||
IDictionary<string, object> attributes,
|
||||
Action<LinkTagHelper> setProperties)
|
||||
{
|
||||
// Arrange
|
||||
var context = MakeTagHelperContext(
|
||||
attributes: new Dictionary<string, object>
|
||||
{
|
||||
{ "asp-fallback-href", "test.css" },
|
||||
{ "asp-fallback-test-class", "hidden" },
|
||||
{ "asp-fallback-test-property", "visible" },
|
||||
{ "asp-fallback-test-value", "hidden" },
|
||||
});
|
||||
var context = MakeTagHelperContext(attributes);
|
||||
var output = MakeTagHelperOutput("link");
|
||||
var logger = new Mock<ILogger<LinkTagHelper>>();
|
||||
|
||||
// Act
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var helper = new LinkTagHelper
|
||||
{
|
||||
Logger = logger.Object,
|
||||
FallbackHref = "test.css",
|
||||
FallbackTestClass = "hidden",
|
||||
FallbackTestProperty = "visible",
|
||||
FallbackTestValue = "hidden"
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
};
|
||||
setProperties(helper);
|
||||
|
||||
// Act
|
||||
helper.Process(context, output);
|
||||
|
||||
// Assert
|
||||
|
|
@ -52,62 +122,135 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var context = MakeTagHelperContext(
|
||||
attributes: new Dictionary<string, object>
|
||||
{
|
||||
{ "rel", "stylesheet"},
|
||||
{ "data-extra", "something"},
|
||||
{ "href", "test.css"},
|
||||
{ "asp-fallback-href", "test.css" },
|
||||
{ "asp-fallback-test-class", "hidden" },
|
||||
{ "asp-fallback-test-property", "visible" },
|
||||
{ "asp-fallback-test-value", "hidden" }
|
||||
["rel"] = "stylesheet",
|
||||
["data-extra"] = "something",
|
||||
["href"] = "test.css",
|
||||
["asp-fallback-href"] = "test.css",
|
||||
["asp-fallback-test-class"] = "hidden",
|
||||
["asp-fallback-test-property"] = "visibility",
|
||||
["asp-fallback-test-value"] = "hidden"
|
||||
});
|
||||
var output = MakeTagHelperOutput("link",
|
||||
attributes: new Dictionary<string, string>
|
||||
{
|
||||
{ "rel", "stylesheet"},
|
||||
{ "data-extra", "something"},
|
||||
{ "href", "test.css"}
|
||||
["rel"] = "stylesheet",
|
||||
["data-extra"] = "something",
|
||||
["href"] = "test.css"
|
||||
});
|
||||
var logger = new Mock<ILogger<LinkTagHelper>>();
|
||||
|
||||
// Act
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var helper = new LinkTagHelper
|
||||
{
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
FallbackHref = "test.css",
|
||||
FallbackTestClass = "hidden",
|
||||
FallbackTestProperty = "visible",
|
||||
FallbackTestProperty = "visibility",
|
||||
FallbackTestValue = "hidden"
|
||||
};
|
||||
|
||||
// Act
|
||||
helper.Process(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.StartsWith("<link rel=\"stylesheet\" data-extra=\"something\" href=\"test.css\"", output.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotRunWhenARequiredAttributeIsMissing()
|
||||
public static TheoryData DoesNotRunWhenARequiredAttributeIsMissing_Data
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<IDictionary<string, object>, Action<LinkTagHelper>>
|
||||
{
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
// This is commented out on purpose: ["asp-href-include"] = "*.css",
|
||||
["asp-href-exclude"] = "*.min.css"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
// This is commented out on purpose: tagHelper.HrefInclude = "*.css";
|
||||
tagHelper.HrefExclude = "*.min.css";
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
// This is commented out on purpose: ["asp-fallback-href"] = "test.css",
|
||||
["asp-fallback-test-class"] = "hidden",
|
||||
["asp-fallback-test-property"] = "visibility",
|
||||
["asp-fallback-test-value"] = "hidden"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
// This is commented out on purpose: tagHelper.FallbackHref = "test.css";
|
||||
tagHelper.FallbackTestClass = "hidden";
|
||||
tagHelper.FallbackTestProperty = "visibility";
|
||||
tagHelper.FallbackTestValue = "hidden";
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-fallback-href"] = "test.css",
|
||||
["asp-fallback-test-class"] = "hidden",
|
||||
// This is commented out on purpose: ["asp-fallback-test-property"] = "visibility",
|
||||
["asp-fallback-test-value"] = "hidden"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.FallbackHref = "test.css";
|
||||
tagHelper.FallbackTestClass = "hidden";
|
||||
// This is commented out on purpose: tagHelper.FallbackTestProperty = "visibility";
|
||||
tagHelper.FallbackTestValue = "hidden";
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
// This is commented out on purpose: ["asp-fallback-href-include"] = "test.css",
|
||||
["asp-fallback-href-exclude"] = "**/*.min.css",
|
||||
["asp-fallback-test-class"] = "hidden",
|
||||
["asp-fallback-test-property"] = "visibility",
|
||||
["asp-fallback-test-value"] = "hidden"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
// This is commented out on purpose: tagHelper.FallbackHrefInclude = "test.css";
|
||||
tagHelper.FallbackHrefExclude = "**/*.min.css";
|
||||
tagHelper.FallbackTestClass = "hidden";
|
||||
tagHelper.FallbackTestProperty = "visibility";
|
||||
tagHelper.FallbackTestValue = "hidden";
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DoesNotRunWhenARequiredAttributeIsMissing_Data))]
|
||||
public void DoesNotRunWhenARequiredAttributeIsMissing(
|
||||
IDictionary<string, object> attributes,
|
||||
Action<LinkTagHelper> setProperties)
|
||||
{
|
||||
// Arrange
|
||||
var context = MakeTagHelperContext(
|
||||
attributes: new Dictionary<string, object>
|
||||
{
|
||||
// This is commented out on purpose: { "asp-fallback-href", "test.css" },
|
||||
{ "asp-fallback-test-class", "hidden" },
|
||||
{ "asp-fallback-test-property", "visible" },
|
||||
{ "asp-fallback-test-value", "hidden" },
|
||||
});
|
||||
var context = MakeTagHelperContext(attributes);
|
||||
var output = MakeTagHelperOutput("link");
|
||||
var logger = new Mock<ILogger<LinkTagHelper>>();
|
||||
|
||||
// Act
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var helper = new LinkTagHelper
|
||||
{
|
||||
Logger = logger.Object,
|
||||
// This is commented out on purpose: FallbackHref = "test.css",
|
||||
FallbackTestClass = "hidden",
|
||||
FallbackTestProperty = "visible",
|
||||
FallbackTestValue = "hidden"
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext
|
||||
};
|
||||
setProperties(helper);
|
||||
|
||||
// Act
|
||||
helper.Process(context, output);
|
||||
|
||||
// Assert
|
||||
|
|
@ -122,12 +265,16 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var context = MakeTagHelperContext();
|
||||
var output = MakeTagHelperOutput("link");
|
||||
var logger = new Mock<ILogger<LinkTagHelper>>();
|
||||
|
||||
// Act
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var helper = new LinkTagHelper
|
||||
{
|
||||
Logger = logger.Object
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext
|
||||
};
|
||||
|
||||
// Act
|
||||
helper.Process(context, output);
|
||||
|
||||
// Assert
|
||||
|
|
@ -135,7 +282,56 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
Assert.False(output.ContentSet);
|
||||
}
|
||||
|
||||
private TagHelperContext MakeTagHelperContext(
|
||||
[Fact]
|
||||
public void RendersLinkTagsForGlobbedHrefResults()
|
||||
{
|
||||
// Arrange
|
||||
var context = MakeTagHelperContext(
|
||||
attributes: new Dictionary<string, object>
|
||||
{
|
||||
["href"] = "/css/site.css",
|
||||
["rel"] = "stylesheet",
|
||||
["asp-href-include"] = "**/*.css"
|
||||
});
|
||||
var output = MakeTagHelperOutput("link", attributes: new Dictionary<string, string>
|
||||
{
|
||||
["href"] = "/css/site.css",
|
||||
["rel"] = "stylesheet"
|
||||
});
|
||||
var logger = new Mock<ILogger<LinkTagHelper>>();
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList("/css/site.css", "**/*.css", null))
|
||||
.Returns(new[] { "/css/site.css", "/base.css" });
|
||||
var helper = new LinkTagHelper
|
||||
{
|
||||
GlobbingUrlBuilder = globbingUrlBuilder.Object,
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
HrefInclude = "**/*.css"
|
||||
};
|
||||
|
||||
// Act
|
||||
helper.Process(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("<link href=\"/css/site.css\" rel=\"stylesheet\" />" +
|
||||
"<link href=\"/base.css\" rel=\"stylesheet\" />", output.Content);
|
||||
}
|
||||
|
||||
private static ViewContext MakeViewContext()
|
||||
{
|
||||
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
var viewData = new ViewDataDictionary(metadataProvider);
|
||||
var viewContext = new ViewContext(actionContext, Mock.Of<IView>(), viewData, TextWriter.Null);
|
||||
|
||||
return viewContext;
|
||||
}
|
||||
|
||||
private static TagHelperContext MakeTagHelperContext(
|
||||
IDictionary<string, object> attributes = null,
|
||||
string content = null)
|
||||
{
|
||||
|
|
@ -144,11 +340,29 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
return new TagHelperContext(attributes, Guid.NewGuid().ToString("N"), () => Task.FromResult(content));
|
||||
}
|
||||
|
||||
private TagHelperOutput MakeTagHelperOutput(string tagName, IDictionary<string, string> attributes = null)
|
||||
private static TagHelperOutput MakeTagHelperOutput(string tagName, IDictionary<string, string> attributes = null)
|
||||
{
|
||||
attributes = attributes ?? new Dictionary<string, string>();
|
||||
|
||||
|
||||
return new TagHelperOutput(tagName, attributes);
|
||||
}
|
||||
|
||||
private static IHostingEnvironment MakeHostingEnvironment(IFileProvider webRootFileProvider = null)
|
||||
{
|
||||
var emptyDirectoryContents = new Mock<IDirectoryContents>();
|
||||
emptyDirectoryContents.Setup(dc => dc.GetEnumerator())
|
||||
.Returns(Enumerable.Empty<IFileInfo>().GetEnumerator());
|
||||
if (webRootFileProvider == null)
|
||||
{
|
||||
var mockFileProvider = new Mock<IFileProvider>();
|
||||
mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny<string>()))
|
||||
.Returns(emptyDirectoryContents.Object);
|
||||
webRootFileProvider = mockFileProvider.Object;
|
||||
}
|
||||
var hostingEnvironment = new Mock<IHostingEnvironment>();
|
||||
hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(webRootFileProvider);
|
||||
|
||||
return hostingEnvironment.Object;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.Framework.Logging;
|
||||
using Xunit;
|
||||
|
|
|
|||
|
|
@ -5,12 +5,114 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Link</title>
|
||||
<link href="~/blank.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="~/link-test.min.css" data-extra="test"
|
||||
asp-fallback-href="~/link-test.css"
|
||||
|
||||
<!-- Plain link tag -->
|
||||
<link href="~/site.css" rel="stylesheet" />
|
||||
|
||||
<!-- Globbed link tag with existing file -->
|
||||
<link asp-href-include="**/site.css" rel="stylesheet" />
|
||||
|
||||
<!-- Globbed link tag with existing file and exclude -->
|
||||
<link asp-href-include="**/*.css" asp-href-exclude="**/site3.css" rel="stylesheet" />
|
||||
|
||||
<!-- Globbed link tag missing include -->
|
||||
<link asp-href-exclude="**/site2.css" rel="stylesheet" />
|
||||
|
||||
<!-- Globbed link tag missing include but with static href -->
|
||||
<link href="~/site.css" asp-href-exclude="**/site2.css" rel="stylesheet" />
|
||||
|
||||
<!-- Globbed link tag with missing file -->
|
||||
<link asp-href-include="**/notThere.css" rel="stylesheet" />
|
||||
|
||||
<!-- Globbed link tag with file outside of webroot -->
|
||||
<link asp-href-include="../**/appRoot.css" rel="stylesheet" />
|
||||
|
||||
<!-- Globbed link tag with file outside of webroot -->
|
||||
<link asp-href-include="**/appRoot.css" rel="stylesheet" />
|
||||
|
||||
<!-- Globbed link tag with existing file and static href -->
|
||||
<link href="~/site.css" asp-href-include="**/site2.css" rel="stylesheet" />
|
||||
|
||||
<!-- Globbed link tag with existing file and static href should dedupe -->
|
||||
<link href="~/site.css" asp-href-include="**/site.css" rel="stylesheet" />
|
||||
|
||||
<!-- Fallback to static href -->
|
||||
<link href="~/site.min.css" rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href="~/site.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-property="visibility"
|
||||
asp-fallback-test-value="hidden" />
|
||||
|
||||
<!-- Fallback to static href with no primary href -->
|
||||
<link rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href="~/site.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-property="visibility"
|
||||
asp-fallback-test-value="hidden" />
|
||||
|
||||
<!-- Fallback to globbed href -->
|
||||
<link href="~/site.min.css" rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href-include="**/site.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-property="visibility"
|
||||
asp-fallback-test-value="hidden" />
|
||||
|
||||
<!-- Fallback to static and globbed href -->
|
||||
<link href="~/site.min.css" rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href="~/site.css"
|
||||
asp-fallback-href-include="**/site2.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-property="visibility"
|
||||
asp-fallback-test-value="hidden" />
|
||||
|
||||
<!-- Fallback to static and globbed href should dedupe -->
|
||||
<link href="~/site.min.css" rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href="~/site.css"
|
||||
asp-fallback-href-include="**/site.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-property="visibility"
|
||||
asp-fallback-test-value="hidden" />
|
||||
|
||||
<!-- Fallback to static and globbed href with exclude -->
|
||||
<link href="~/site.min.css" rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href="~/site.css"
|
||||
asp-fallback-href-include="**/*.css"
|
||||
asp-fallback-href-exclude="**/site3.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-property="visibility"
|
||||
asp-fallback-test-value="hidden" />
|
||||
|
||||
<!-- Fallback to globbed href that doesn't exist -->
|
||||
<link href="~/site.min.css" rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href-include="**/notThere.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-property="visibility"
|
||||
asp-fallback-test-value="hidden" />
|
||||
|
||||
<!-- Fallback to globbed href outside webroot -->
|
||||
<link href="~/site.min.css" rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href-include="../**/appRoot.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-property="visibility"
|
||||
asp-fallback-test-value="hidden" />
|
||||
|
||||
<!-- Fallback with missing attribute -->
|
||||
<link href="~/site.min.css" rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href="~/site.css"
|
||||
asp-fallback-test-property="visibility"
|
||||
asp-fallback-test-value="hidden" />
|
||||
|
||||
<!-- Fallback with missing attribute -->
|
||||
<link href="~/site.min.css" rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href="~/site.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-value="hidden" />
|
||||
|
||||
<!-- Fallback with missing attribute -->
|
||||
<link href="~/site.min.css" rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href="~/site.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-property="visibility" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
body::after {
|
||||
display: block;
|
||||
background-color: #ff0000;
|
||||
color: #fff;
|
||||
font-size: 1.2em;
|
||||
margin-top: 2.4em;
|
||||
content: "ERROR: Stylesheet 'appRoot.css' was loaded from outside webroot!";
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
body::after {
|
||||
display: block;
|
||||
color: #0fa912;
|
||||
margin-top: 2.4em;
|
||||
content: "Stylesheet 'site.css' loaded successfully!";
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
body::after {
|
||||
display: block;
|
||||
color: #0fa912;
|
||||
margin-top: 2.4em;
|
||||
content: "Stylesheet 'site2.css' loaded successfully!";
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
body::after {
|
||||
display: block;
|
||||
background-color: #ff0000;
|
||||
color: #fff;
|
||||
margin-top: 2.4em;
|
||||
content: "ERROR: Stylesheet 'site3.css' was loaded despite being excluded!";
|
||||
}
|
||||
Loading…
Reference in New Issue