-
-
+
+
@RenderBody()
diff --git a/samples/TagHelperSample.Web/appRoot.css b/samples/TagHelperSample.Web/appRoot.css
new file mode 100644
index 0000000000..7b9214822c
--- /dev/null
+++ b/samples/TagHelperSample.Web/appRoot.css
@@ -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!";
+}
\ No newline at end of file
diff --git a/samples/TagHelperSample.Web/project.json b/samples/TagHelperSample.Web/project.json
index 628f1f61f2..5e9c73e3fc 100644
--- a/samples/TagHelperSample.Web/project.json
+++ b/samples/TagHelperSample.Web/project.json
@@ -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": {
diff --git a/samples/TagHelperSample.Web/wwwroot/blank.css b/samples/TagHelperSample.Web/wwwroot/blank.css
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/samples/TagHelperSample.Web/wwwroot/css/sub/blank.css b/samples/TagHelperSample.Web/wwwroot/css/sub/blank.css
new file mode 100644
index 0000000000..2d91681f81
--- /dev/null
+++ b/samples/TagHelperSample.Web/wwwroot/css/sub/blank.css
@@ -0,0 +1 @@
+body {}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs
new file mode 100644
index 0000000000..ee32847445
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs
@@ -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
+{
+ ///
+ /// Methods for determining how an should run based on the attributes that were specified.
+ ///
+ public static class AttributeMatcher
+ {
+ ///
+ /// Determines whether a 's required attributes are present, non null, non empty, and
+ /// non whitepsace.
+ ///
+ /// The .
+ ///
+ /// The attributes the requires in order to run.
+ ///
+ /// An optional to log warning details to.
+ /// A indicating whether the should run.
+ public static bool AllRequiredAttributesArePresent(
+ [NotNull] TagHelperContext context,
+ [NotNull] IEnumerable 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;
+ }
+
+ ///
+ /// Determines the modes a can run in based on which modes have all their required
+ /// attributes present, non null, non empty, and non whitepsace.
+ ///
+ /// The type representing the 's modes.
+ /// The .
+ /// The modes and their required attributes.
+ /// The .
+ public static ModeMatchResult DetermineMode(
+ [NotNull] TagHelperContext context,
+ [NotNull] IEnumerable> modeInfos)
+ {
+ // true == full match, false == partial match
+ var matchedAttributes = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var result = new ModeMatchResult();
+
+ 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 requiredAttributes)
+ {
+ // Check for all attribute values
+ var presentAttributes = new List();
+ var missingAttributes = new List();
+
+ 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 Present { get; set; }
+
+ public IEnumerable Missing { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileProviderGlobbingDirectory.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileProviderGlobbingDirectory.cs
new file mode 100644
index 0000000000..f5b50f4925
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileProviderGlobbingDirectory.cs
@@ -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 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileProviderGlobbingFile.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileProviderGlobbingFile.cs
new file mode 100644
index 0000000000..3b8c9c6291
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileProviderGlobbingFile.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/GlobbingUrlBuilder.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/GlobbingUrlBuilder.cs
new file mode 100644
index 0000000000..2869d66c5b
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/GlobbingUrlBuilder.cs
@@ -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
+{
+ ///
+ /// Utility methods for 's that support attributes containing file globbing patterns.
+ ///
+ public class GlobbingUrlBuilder
+ {
+ private static readonly char[] PatternSeparator = new[] { ',' };
+
+ private readonly FileProviderGlobbingDirectory _baseGlobbingDirectory;
+
+ // Internal for testing
+ internal GlobbingUrlBuilder() { }
+
+ ///
+ /// Creates a new .
+ ///
+ /// The file provider.
+ /// The cache.
+ /// The request path base.
+ public GlobbingUrlBuilder([NotNull] IFileProvider fileProvider, IMemoryCache cache, PathString requestPathBase)
+ {
+ FileProvider = fileProvider;
+ Cache = cache;
+ RequestPathBase = requestPathBase;
+ _baseGlobbingDirectory = new FileProviderGlobbingDirectory(fileProvider, fileInfo: null, parent: null);
+ }
+
+ ///
+ /// The to cache globbing results in.
+ ///
+ public IMemoryCache Cache { get; }
+
+ ///
+ /// The used to watch for changes to file globbing results.
+ ///
+ public IFileProvider FileProvider { get; }
+
+ ///
+ /// The base path of the current request (i.e. ).
+ ///
+ public PathString RequestPathBase { get; }
+
+ // Internal for testing.
+ internal Func MatcherBuilder { get; set; }
+
+ ///
+ /// Builds a list of URLs.
+ ///
+ /// The statically declared URL. This will always be added to the result.
+ /// The file globbing include pattern.
+ /// The file globbing exclude pattern.
+ /// The list of URLs
+ public virtual IEnumerable BuildUrlList(string staticUrl, string includePattern, string excludePattern)
+ {
+ var urls = new HashSet(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 ExpandGlobbedUrl(string include, string exclude)
+ {
+ if (string.IsNullOrEmpty(include))
+ {
+ return Enumerable.Empty();
+ }
+
+ var includePatterns = include.Split(PatternSeparator, StringSplitOptions.RemoveEmptyEntries);
+ var excludePatterns = exclude?.Split(PatternSeparator, StringSplitOptions.RemoveEmptyEntries);
+
+ if (includePatterns.Length == 0)
+ {
+ return Enumerable.Empty();
+ }
+
+ 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 FindFiles(IEnumerable includePatterns, IEnumerable 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/JavaScriptResources.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/JavaScriptResources.cs
new file mode 100644
index 0000000000..fd1b5ffb0d
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/JavaScriptResources.cs
@@ -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
+{
+ ///
+ /// Methods for loading JavaScript from assembly embedded resources.
+ ///
+ public static class JavaScriptResources
+ {
+ private static readonly Assembly ResourcesAssembly = typeof(JavaScriptResources).GetTypeInfo().Assembly;
+
+ private static readonly ConcurrentDictionary Cache =
+ new ConcurrentDictionary(StringComparer.Ordinal);
+
+ ///
+ /// Gets an embedded JavaScript file resource and decodes it for use as a .NET format string.
+ ///
+ public static string GetEmbeddedJavaScript(string resourceName)
+ {
+ return GetEmbeddedJavaScript(resourceName, ResourcesAssembly.GetManifestResourceStream, Cache);
+ }
+
+ // Internal for testing
+ internal static string GetEmbeddedJavaScript(
+ string resourceName,
+ Func getManifestResourceStream,
+ ConcurrentDictionary 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("]]]", "}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/JavaScriptStringArrayEncoder.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/JavaScriptStringArrayEncoder.cs
new file mode 100644
index 0000000000..15c4e358ef
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/JavaScriptStringArrayEncoder.cs
@@ -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
+{
+ ///
+ /// Methods for encoding for use as a JavaScript array literal.
+ ///
+ public static class JavaScriptStringArrayEncoder
+ {
+ ///
+ /// Encodes a .NET string array for safe use as a JavaScript array literal, including inline in an HTML file.
+ ///
+ public static string Encode(IJavaScriptStringEncoder encoder, IEnumerable 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/MissingAttributeLoggerStructure.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/MissingAttributeLoggerStructure.cs
similarity index 86%
rename from src/Microsoft.AspNet.Mvc.TagHelpers/MissingAttributeLoggerStructure.cs
rename to src/Microsoft.AspNet.Mvc.TagHelpers/Internal/MissingAttributeLoggerStructure.cs
index 228e05e144..b5818101c6 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/MissingAttributeLoggerStructure.cs
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/MissingAttributeLoggerStructure.cs
@@ -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
{
///
- /// An for log messages regarding instances that opt out of
+ /// An for log messages regarding instances that opt out of
/// processing due to missing required attributes.
///
public class MissingAttributeLoggerStructure : ILoggerStructure
@@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
private readonly string _uniqueId;
private readonly IEnumerable> _values;
- // internal for unit testing.
+ // Internal for unit testing
internal IEnumerable MissingAttributes { get; }
///
@@ -30,8 +30,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
MissingAttributes = missingAttributes;
_values = new Dictionary
{
- { "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.";
}
}
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeAttributes.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeAttributes.cs
new file mode 100644
index 0000000000..85b178c728
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeAttributes.cs
@@ -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
+{
+ ///
+ /// Static creation methods for .
+ ///
+ public static class ModeAttributes
+ {
+ ///
+ /// Creates an /
+ ///
+ public static ModeAttributes Create(TMode mode, IEnumerable attributes)
+ {
+ return new ModeAttributes
+ {
+ Mode = mode,
+ Attributes = attributes
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeAttributesOfT.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeAttributesOfT.cs
new file mode 100644
index 0000000000..32578d3ef4
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeAttributesOfT.cs
@@ -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
+{
+ ///
+ /// A mapping of a mode to its required attributes.
+ ///
+ /// The type representing the 's mode.
+ public class ModeAttributes
+ {
+ ///
+ /// The 's mode.
+ ///
+ public TMode Mode { get; set; }
+
+ ///
+ /// The names of attributes required for this mode.
+ ///
+ public IEnumerable Attributes { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeMatchAttributes.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeMatchAttributes.cs
new file mode 100644
index 0000000000..409bb6d86d
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeMatchAttributes.cs
@@ -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
+{
+ ///
+ /// Static creation methods for .
+ ///
+ public static class ModeMatchAttributes
+ {
+ ///
+ /// Creates an .
+ ///
+ public static ModeMatchAttributes Create(
+ TMode mode,
+ IEnumerable presentAttributes)
+ {
+ return Create(mode, presentAttributes, missingAttributes: null);
+ }
+
+ ///
+ /// Creates an .
+ ///
+ public static ModeMatchAttributes Create(
+ TMode mode,
+ IEnumerable presentAttributes,
+ IEnumerable missingAttributes)
+ {
+ return new ModeMatchAttributes
+ {
+ Mode = mode,
+ PresentAttributes = presentAttributes,
+ MissingAttributes = missingAttributes
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeMatchAttributesOfT.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeMatchAttributesOfT.cs
new file mode 100644
index 0000000000..82bbc56d88
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeMatchAttributesOfT.cs
@@ -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
+{
+ ///
+ /// A mapping of a mode to its missing and present attributes.
+ ///
+ /// The type representing the 's mode.
+ public class ModeMatchAttributes
+ {
+ ///
+ /// The 's mode.
+ ///
+ public TMode Mode { get; set; }
+
+ ///
+ /// The names of attributes that were present in this match.
+ ///
+ public IEnumerable PresentAttributes { get; set; }
+
+ ///
+ /// The names of attributes that were missing in this match.
+ ///
+ public IEnumerable MissingAttributes { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeMatchResult.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeMatchResult.cs
new file mode 100644
index 0000000000..085640eb2a
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/ModeMatchResult.cs
@@ -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
+{
+ ///
+ /// Result of determining the mode an will run in.
+ ///
+ /// The type representing the 's mode.
+ public class ModeMatchResult
+ {
+ ///
+ /// Modes that were missing attributes but had at least one attribute present.
+ ///
+ public IList> PartialMatches { get; } = new List>();
+
+ ///
+ /// Modes that had all attributes present.
+ ///
+ public IList> FullMatches { get; } = new List>();
+
+ ///
+ /// Attributes that are present in at least one mode in , but in no modes in
+ /// .
+ ///
+ public IList PartiallyMatchedAttributes { get; } = new List();
+
+ ///
+ /// Logs the details of the .
+ ///
+ /// The .
+ /// The .
+ /// The value of .
+ public void LogDetails([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(uniqueId, partialOnlyMatches));
+ }
+
+ if (logger.IsEnabled(LogLevel.Verbose) && !FullMatches.Any())
+ {
+ logger.WriteVerbose("Skipping processing for {0} {1}",
+ tagHelper.GetType().GetTypeInfo().FullName, uniqueId);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialAttributeLoggerStructure.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialAttributeLoggerStructure.cs
new file mode 100644
index 0000000000..c4e0a8eaba
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/PartialAttributeLoggerStructure.cs
@@ -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
+{
+ ///
+ /// An for log messages regarding instances that opt out of
+ /// processing due to missing attributes for one of several possible modes.
+ ///
+ public class PartialModeMatchLoggerStructure : ILoggerStructure
+ {
+ private readonly string _uniqueId;
+ private readonly IEnumerable> _partialMatches;
+ private readonly IEnumerable> _values;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The unique ID of the HTML element this message applies to.
+ /// The set of modes with partial required attributes.
+ public PartialModeMatchLoggerStructure(
+ string uniqueId,
+ [NotNull] IEnumerable> partialMatches)
+ {
+ _uniqueId = uniqueId;
+ _partialMatches = partialMatches;
+ _values = new Dictionary
+ {
+ ["UniqueId"] = _uniqueId,
+ ["PartialMatches"] = partialMatches
+ };
+ }
+
+ ///
+ /// The log message.
+ ///
+ public string Message
+ {
+ get
+ {
+ return "Tag Helper has missing required attributes.";
+ }
+ }
+
+ ///
+ /// Gets the values associated with this structured log message.
+ ///
+ /// The values.
+ public IEnumerable> GetValues()
+ {
+ return _values;
+ }
+
+ ///
+ /// Generates a human readable string for this structured log message.
+ ///
+ /// The message.
+ 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)))));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/JavaScriptUtility.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/JavaScriptUtility.cs
deleted file mode 100644
index dfeb359a2d..0000000000
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/JavaScriptUtility.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Utility methods for dealing with JavaScript.
- ///
- public static class JavaScriptUtility
- {
- private static readonly Assembly ResourcesAssembly = typeof(JavaScriptUtility).GetTypeInfo().Assembly;
-
- private static readonly ConcurrentDictionary Cache =
- new ConcurrentDictionary(StringComparer.Ordinal);
-
- private static readonly IDictionary EncodingMap = new Dictionary
- {
- { '<', @"\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
- };
-
- ///
- /// Gets an embedded JavaScript file resource and decodes it for use as a .NET format string.
- ///
- 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("]]]", "}");
- }
-
- ///
- /// Encodes a .NET string for safe use as a JavaScript string literal, including inline in an HTML file.
- ///
- 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);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs
index 0f09635b80..1fe47b3082 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs
@@ -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
///
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 = "";
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[] 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
+ {
+ ///
+ /// Rendering a fallback block if primary stylesheet fails to load. Will also do globbing if the appropriate
+ /// properties are set.
+ ///
+ Fallback,
+ ///
+ /// Just performing file globbing search for the href, rendering a separate <link> for each match.
+ ///
+ GlobbedHref
+ }
+
+ ///
+ /// 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.
+ ///
+ [HtmlAttributeName(HrefIncludeAttributeName)]
+ public string HrefInclude { get; set; }
+
+ ///
+ /// 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 .
+ ///
+ [HtmlAttributeName(HrefExcludeAttributeName)]
+ public string HrefExclude { get; set; }
+
///
/// 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; }
+ ///
+ /// 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.
+ ///
+ [HtmlAttributeName(FallbackHrefIncludeAttributeName)]
+ public string FallbackHrefInclude { get; set; }
+
+ ///
+ /// 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 .
+ ///
+ [HtmlAttributeName(FallbackHrefExcludeAttributeName)]
+ public string FallbackHrefExclude { get; set; }
+
///
/// The class name defined in the stylesheet to use for the fallback test.
+ /// Must be used in conjunction with and ,
+ /// and either or .
///
[HtmlAttributeName(FallbackTestClassAttributeName)]
public string FallbackTestClass { get; set; }
///
/// The CSS property name to use for the fallback test.
+ /// Must be used in conjunction with and ,
+ /// and either or .
///
[HtmlAttributeName(FallbackTestPropertyAttributeName)]
public string FallbackTestProperty { get; set; }
///
/// The CSS property value to use for the fallback test.
+ /// Must be used in conjunction with and ,
+ /// and either or .
///
[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 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; }
+
///
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(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 tag that loads the primary stylesheet
- content.Append(" tag to match the original one in the source file
+ BuildLinkTag(attributes, builder);
+ }
+ else
+ {
+ BuildGlobbedLinkTags(attributes, builder);
}
- content.AppendLine("/>");
- // Build the 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 tag that checks the effective style of tag above and renders the extra
- // 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("");
+ // 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 attributes, StringBuilder builder)
+ {
+ // Build a 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 tag that's used to test for the presence of the stylesheet
+ builder.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "",
+ WebUtility.HtmlEncode(FallbackTestClass));
+
+ // Build the tag that checks the effective style of tag above and renders the extra
+ // 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("");
+ }
+ }
+
+ private void EnsureGlobbingUrlBuilder()
+ {
+ if (GlobbingUrlBuilder == null)
+ {
+ GlobbingUrlBuilder = new GlobbingUrlBuilder(
+ HostingEnvironment.WebRootFileProvider,
+ Cache,
+ ViewContext.HttpContext.Request.PathBase);
+ }
+ }
+
+ private static void BuildLinkTag(IDictionary attributes, StringBuilder builder)
+ {
+ builder.Append("");
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/AssemblyInfo.cs
index 58841d9458..274352fe87 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/AssemblyInfo.cs
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/AssemblyInfo.cs
@@ -4,3 +4,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.TagHelpers.Test")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs
index eac3a38d69..fda4d1cdc8 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs
@@ -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
///
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);
}
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperContextExtensions.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperContextExtensions.cs
deleted file mode 100644
index e1bde2e51b..0000000000
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperContextExtensions.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Utility related extensions for .
- ///
- public static class TagHelperContextExtensions
- {
- ///
- /// Determines whether a 's required attributes are present, non null, non empty, and
- /// non whitepsace.
- ///
- /// The .
- /// The attributes the requires in order to run.
- /// An optional to log warning details to.
- /// A indicating whether the should run.
- public static bool AllRequiredAttributesArePresent(
- [NotNull]this TagHelperContext context,
- [NotNull]IEnumerable requiredAttributes,
- ILogger logger = null)
- {
- // Check for all attribute values & log a warning if any required are missing
- var atLeastOnePresent = false;
- var missingAttrNames = new List();
-
- 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;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js b/src/Microsoft.AspNet.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js
index 0dc37d59f0..e68f3a92d5 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js
@@ -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('');
+ for (i = 0; i < fallbackHref.length; i++) {
+ doc.write('');
+ }
}
-})("[[[0]]]", "[[[1]]]", "[[[2]]]");
\ No newline at end of file
+})("[[[0]]]", "[[[1]]]", [[[2]]]);
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/project.json b/src/Microsoft.AspNet.Mvc.TagHelpers/project.json
index c32fd00f7b..27d305906d 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/project.json
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/project.json
@@ -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": {
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html
index 07e924e710..d06c5a376c 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html
@@ -4,10 +4,75 @@
Link
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Internal/AttributeMatcherTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Internal/AttributeMatcherTest.cs
new file mode 100644
index 0000000000..964e820464
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Internal/AttributeMatcherTest.cs
@@ -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
+ {
+ ["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
+ {
+ ["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
+ {
+ ["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 attributes = null,
+ string content = null)
+ {
+ attributes = attributes ?? new Dictionary();
+
+ return new TagHelperContext(attributes, Guid.NewGuid().ToString("N"), () => Task.FromResult(content));
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Internal/GlobbingUrlBuilderTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Internal/GlobbingUrlBuilderTest.cs
new file mode 100644
index 0000000000..62bc4925c4
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Internal/GlobbingUrlBuilderTest.cs
@@ -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 { "/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();
+ var fileProvider = MakeFileProvider(MakeDirectoryContents("site.css", "blank.css"));
+ Mock.Get(fileProvider).Setup(f => f.Watch(It.IsAny())).Returns(trigger.Object);
+ var cache = MakeCache();
+ var cacheSetContext = new Mock();
+ cacheSetContext.Setup(c => c.AddExpirationTrigger(trigger.Object)).Verifiable();
+ Mock.Get(cache).Setup(c => c.Set(
+ /*key*/ It.IsAny(),
+ /*link*/ It.IsAny(),
+ /*state*/ It.IsAny