Add support for globbing to ScriptTagHelper:

- Fixes #1577
This commit is contained in:
damianedwards 2015-02-27 10:37:36 -08:00
parent 6490b113d8
commit 8b64e3c7bb
17 changed files with 816 additions and 326 deletions

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Framework.Internal;
using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{
@ -15,44 +14,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// </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="tagHelperContext">The <see cref="TagHelperContext"/>.</param>
/// <param name="viewContext">The <see cref="ViewContext"/>.</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 tagHelperContext,
[NotNull] ViewContext viewContext,
[NotNull] IEnumerable<string> requiredAttributes,
ILogger logger)
{
var attributes = GetPresentMissingAttributes(tagHelperContext, 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(
tagHelperContext.UniqueId,
viewContext.View.Path,
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.

View File

@ -1,74 +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.Collections.Generic;
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 required attributes.
/// </summary>
public class MissingAttributeLoggerStructure : ILoggerStructure
{
private readonly string _uniqueId;
private readonly string _viewPath;
private readonly IEnumerable<KeyValuePair<string, object>> _values;
// Internal for unit testing
internal IEnumerable<string> MissingAttributes { get; }
/// <summary>
/// Creates a new <see cref="MissingAttributeLoggerStructure"/>.
/// </summary>
/// <param name="uniqueId">The unique ID of the HTML element this message applies to.</param>
/// <param name="viewPath">The path to the view.</param>
/// <param name="missingAttributes">The missing required attributes.</param>
public MissingAttributeLoggerStructure(string uniqueId, string viewPath, IEnumerable<string> missingAttributes)
{
_uniqueId = uniqueId;
_viewPath = viewPath;
MissingAttributes = missingAttributes;
_values = new Dictionary<string, object>
{
["UniqueId"] = _uniqueId,
["ViewPath"] = _viewPath,
["MissingAttributes"] = MissingAttributes
};
}
/// <summary>
/// The log message.
/// </summary>
public string Message
{
get
{
return "Tag Helper has one or more 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()
{
return string.Format("Tag Helper with ID {0} in view '{1}' is missing attributes: {2}",
_uniqueId,
_viewPath,
string.Join(",", MissingAttributes));
}
}
}

View File

@ -14,12 +14,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// 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
public class PartialModeMatchLoggerStructure<TMode> : PartialModeMatchLoggerStructure
{
private readonly string _uniqueId;
private readonly string _viewPath;
private readonly IEnumerable<ModeMatchAttributes<TMode>> _partialMatches;
private readonly IEnumerable<KeyValuePair<string, object>> _values;
/// <summary>
/// Creates a new <see cref="PartialModeMatchLoggerStructure{TMode}"/>.
@ -31,43 +30,23 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
string uniqueId,
string viewPath,
[NotNull] IEnumerable<ModeMatchAttributes<TMode>> partialMatches)
: base(values: new Dictionary<string, object>
{
["UniqueId"] = uniqueId,
["ViewPath"] = viewPath,
["PartialMatches"] = partialMatches
})
{
_uniqueId = uniqueId;
_viewPath = viewPath;
_partialMatches = partialMatches;
_values = new Dictionary<string, object>
{
["UniqueId"] = _uniqueId,
["ViewPath"] = _viewPath,
["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()
public override string Format()
{
var newLine = Environment.NewLine;
return string.Format(

View File

@ -0,0 +1,48 @@
// 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;
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 abstract class PartialModeMatchLoggerStructure : ILoggerStructure
{
private readonly IEnumerable<KeyValuePair<string, object>> _values;
protected PartialModeMatchLoggerStructure(IEnumerable<KeyValuePair<string, object>> values)
{
_values = values;
}
/// <summary>
/// The log message.
/// </summary>
public string Message
{
get
{
return "Tag Helper has missing required attributes.";
}
}
/// <summary>
/// Returns a human-readable string of the structured data.
/// </summary>
public abstract string Format();
/// <summary>
/// Gets the values associated with this structured log message.
/// </summary>
/// <returns>The values.</returns>
public IEnumerable<KeyValuePair<string, object>> GetValues()
{
return _values;
}
}
}

View File

@ -66,15 +66,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
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 &lt;link&gt; for each match.
/// </summary>
GlobbedHref
GlobbedHref = 0,
/// <summary>
/// Rendering a fallback block if primary stylesheet fails to load. Will also do globbing for both the
/// primary and fallback hrefs if the appropriate properties are set.
/// </summary>
Fallback = 1,
}
/// <summary>
@ -93,15 +93,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
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).
/// The URL of a CSS stylesheet to fallback to in the case the primary one fails.
/// </summary>
[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).
/// one fails.
/// The glob patterns are assessed relative to the application's 'webroot' setting.
/// </summary>
[HtmlAttributeName(FallbackHrefIncludeAttributeName)]
@ -109,7 +108,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <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 case the primary one fails.
/// The glob patterns are assessed relative to the application's 'webroot' setting.
/// Must be used in conjunction with <see cref="FallbackHrefInclude"/>.
/// </summary>
@ -161,9 +160,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
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, ViewContext.View.Path);
if (!modeResult.FullMatches.Any())
@ -172,7 +168,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
return;
}
var mode = modeResult.FullMatches.First().Mode;
// Get the highest matched mode
var mode = modeResult.FullMatches.Select(match => match.Mode).Max();
// NOTE: Values in TagHelperOutput.Attributes are already HtmlEncoded
var attributes = new Dictionary<string, string>(output.Attributes);
@ -206,11 +203,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
attributes.TryGetValue("href", out staticHref);
EnsureGlobbingUrlBuilder();
var hrefs = GlobbingUrlBuilder.BuildUrlList(staticHref, HrefInclude, HrefExclude);
var urls = GlobbingUrlBuilder.BuildUrlList(staticHref, HrefInclude, HrefExclude);
foreach (var href in hrefs)
foreach (var url in urls)
{
attributes["href"] = WebUtility.HtmlEncode(href);
attributes["href"] = WebUtility.HtmlEncode(url);
BuildLinkTag(attributes, builder);
}
}

View File

@ -2,12 +2,17 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Framework.Cache.Memory;
using Microsoft.Framework.Logging;
using Microsoft.Framework.WebEncoders;
@ -22,24 +27,92 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// </remarks>
public class ScriptTagHelper : TagHelper
{
private const string SrcIncludeAttributeName = "asp-src-include";
private const string SrcExcludeAttributeName = "asp-src-exclude";
private const string FallbackSrcAttributeName = "asp-fallback-src";
private const string FallbackSrcIncludeAttributeName = "asp-fallback-src-include";
private const string FallbackSrcExcludeAttributeName = "asp-fallback-src-exclude";
private const string FallbackTestExpressionAttributeName = "asp-fallback-test";
private const string SrcAttributeName = "src";
// NOTE: All attributes are required for the ScriptTagHelper to process.
private static readonly string[] RequiredAttributes = new[]
{
FallbackSrcAttributeName,
FallbackTestExpressionAttributeName,
private static readonly ModeAttributes<Mode>[] ModeDetails = new[] {
// Globbed src (include only)
ModeAttributes.Create(Mode.GlobbedSrc, new [] { SrcIncludeAttributeName }),
// Globbed src (include & exclude)
ModeAttributes.Create(Mode.GlobbedSrc, new [] { SrcIncludeAttributeName, SrcExcludeAttributeName }),
// Fallback with static src
ModeAttributes.Create(
Mode.Fallback, new[]
{
FallbackSrcAttributeName,
FallbackTestExpressionAttributeName
}),
// Fallback with globbed src (include only)
ModeAttributes.Create(
Mode.Fallback, new[] {
FallbackSrcIncludeAttributeName,
FallbackTestExpressionAttributeName
}),
// Fallback with globbed src (include & exclude)
ModeAttributes.Create(
Mode.Fallback, new[] {
FallbackSrcIncludeAttributeName,
FallbackSrcExcludeAttributeName,
FallbackTestExpressionAttributeName
}),
};
private enum Mode
{
/// <summary>
/// Just performing file globbing search for the src, rendering a separate &lt;script&gt; for each match.
/// </summary>
GlobbedSrc = 0,
/// <summary>
/// Rendering a fallback block if primary javascript fails to load. Will also do globbing for both the
/// primary and fallback srcs if the appropriate properties are set.
/// </summary>
Fallback = 1
}
/// <summary>
/// The URL of a Script tag to fallback to in the case the primary one fails (as specified in the src
/// attribute).
/// A comma separated list of globbed file patterns of JavaScript scripts to load.
/// The glob patterns are assessed relative to the application's 'webroot' setting.
/// </summary>
[HtmlAttributeName(SrcIncludeAttributeName)]
public string SrcInclude { get; set; }
/// <summary>
/// A comma separated list of globbed file patterns of JavaScript scripts to exclude from loading.
/// The glob patterns are assessed relative to the application's 'webroot' setting.
/// Must be used in conjunction with <see cref="SrcInclude"/>.
/// </summary>
[HtmlAttributeName(SrcExcludeAttributeName)]
public string SrcExclude { get; set; }
/// <summary>
/// The URL of a Script tag to fallback to in the case the primary one fails.
/// </summary>
[HtmlAttributeName(FallbackSrcAttributeName)]
public string FallbackSrc { get; set; }
/// <summary>
/// A comma separated list of globbed file patterns of JavaScript scripts to fallback to in the case the
/// primary one fails.
/// The glob patterns are assessed relative to the application's 'webroot' setting.
/// </summary>
[HtmlAttributeName(FallbackSrcIncludeAttributeName)]
public string FallbackSrcInclude { get; set; }
/// <summary>
/// A comma separated list of globbed file patterns of JavaScript scripts to exclude from the fallback list, in
/// the case the primary one fails.
/// The glob patterns are assessed relative to the application's 'webroot' setting.
/// Must be used in conjunction with <see cref="FallbackSrcInclude"/>.
/// </summary>
[HtmlAttributeName(FallbackSrcExcludeAttributeName)]
public string FallbackSrcExclude { get; set; }
/// <summary>
/// The script method defined in the primary script to use for the fallback test.
/// </summary>
@ -50,84 +123,167 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[Activate]
protected internal ILogger<ScriptTagHelper> 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 async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (!AttributeMatcher.AllRequiredAttributesArePresent(context, ViewContext, RequiredAttributes, Logger))
{
if (Logger.IsEnabled(LogLevel.Verbose))
{
Logger.WriteVerbose(
"Skipping processing for {0} with ID {1} on view '{2}'",
nameof(ScriptTagHelper),
context.UniqueId,
ViewContext.View.Path);
}
var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails);
modeResult.LogDetails(Logger, this, context.UniqueId, ViewContext.View.Path);
if (!modeResult.FullMatches.Any())
{
// No attributes matched so we have nothing to do
return;
}
var content = new StringBuilder();
// NOTE: Values in Output.Attributes are already HtmlEncoded
// We've taken over rendering here so prevent the element rendering the outer tag
output.TagName = null;
// Rebuild the <script /> tag.
content.Append("<script");
foreach (var attribute in output.Attributes)
{
content.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", attribute.Key, attribute.Value);
}
content.Append(">");
// Get the highest matched mode
var mode = modeResult.FullMatches.Select(match => match.Mode).Max();
// NOTE: Values in TagHelperOutput.Attributes are already HtmlEncoded
var attributes = new Dictionary<string, string>(output.Attributes);
var builder = new StringBuilder();
var originalContent = await context.GetChildContentAsync();
content.Append(originalContent)
.AppendLine("</script>");
// Build the <script> tag that checks the test method and if it fails, renders the extra script.
content.Append("<script>(")
.Append(FallbackTestExpression)
.Append("||document.write(\"<script");
if (!output.Attributes.ContainsKey("src"))
if (mode == Mode.Fallback && string.IsNullOrEmpty(SrcInclude))
{
AppendSrc(content, "src");
// No globbing to do, just build a <script /> tag to match the original one in the source file
BuildScriptTag(originalContent, attributes, builder);
}
else
{
BuildGlobbedScriptTags(originalContent, attributes, builder);
}
foreach (var attribute in output.Attributes)
if (mode == Mode.Fallback)
{
if (!attribute.Key.Equals(SrcAttributeName, StringComparison.OrdinalIgnoreCase))
{
var encodedKey = JavaScriptStringEncoder.Default.JavaScriptStringEncode(attribute.Key);
var encodedValue = JavaScriptStringEncoder.Default.JavaScriptStringEncode(attribute.Value);
content.AppendFormat(CultureInfo.InvariantCulture, " {0}=\\\"{1}\\\"", encodedKey, encodedValue);
}
else
{
AppendSrc(content, attribute.Key);
}
BuildFallbackBlock(attributes, builder);
}
content.Append("><\\/script>\"));</script>");
output.Content = content.ToString();
// We've taken over tag rendering, so prevent rendering the outer tag
output.TagName = null;
output.Content = builder.ToString();
}
private void AppendSrc(StringBuilder content, string srcKey)
private void BuildGlobbedScriptTags(
string originalContent,
IDictionary<string, string> attributes,
StringBuilder builder)
{
// Build a <script> tag for each matched src as well as the original one in the source file
string staticSrc;
attributes.TryGetValue("src", out staticSrc);
EnsureGlobbingUrlBuilder();
var urls = GlobbingUrlBuilder.BuildUrlList(staticSrc, SrcInclude, SrcExclude);
foreach (var url in urls)
{
attributes["src"] = WebUtility.HtmlEncode(url);
var content = string.Equals(url, staticSrc, StringComparison.OrdinalIgnoreCase)
? originalContent
: string.Empty;
BuildScriptTag(content, attributes, builder);
}
}
private void BuildFallbackBlock(IDictionary<string, string> attributes, StringBuilder builder)
{
EnsureGlobbingUrlBuilder();
var fallbackSrcs = GlobbingUrlBuilder.BuildUrlList(FallbackSrc, FallbackSrcInclude, FallbackSrcExclude);
if (fallbackSrcs.Any())
{
// Build the <script> tag that checks the test method and if it fails, renders the extra script.
builder.AppendLine()
.Append("<script>(")
.Append(FallbackTestExpression)
.Append("||document.write(\"");
foreach (var src in fallbackSrcs)
{
builder.Append("<script");
if (!attributes.ContainsKey("src"))
{
AppendSrc(builder, "src", src);
}
foreach (var attribute in attributes)
{
if (!attribute.Key.Equals(SrcAttributeName, StringComparison.OrdinalIgnoreCase))
{
var encodedKey = JavaScriptStringEncoder.Default.JavaScriptStringEncode(attribute.Key);
var encodedValue = JavaScriptStringEncoder.Default.JavaScriptStringEncode(attribute.Value);
builder.AppendFormat(
CultureInfo.InvariantCulture,
" {0}=\\\"{1}\\\"",
encodedKey,
encodedValue);
}
else
{
AppendSrc(builder, attribute.Key, src);
}
}
builder.Append("><\\/script>");
}
builder.Append("\"));</script>");
}
}
private void EnsureGlobbingUrlBuilder()
{
if (GlobbingUrlBuilder == null)
{
GlobbingUrlBuilder = new GlobbingUrlBuilder(
HostingEnvironment.WebRootFileProvider,
Cache,
ViewContext.HttpContext.Request.PathBase);
}
}
private static void BuildScriptTag(
string content,
IDictionary<string, string> attributes,
StringBuilder builder)
{
builder.Append("<script");
foreach (var attribute in attributes)
{
builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", attribute.Key, attribute.Value);
}
builder.Append(">")
.Append(content)
.Append("</script>");
}
private void AppendSrc(StringBuilder content, string srcKey, string srcValue)
{
// Append src attribute in the original place and replace the content the fallback content
// No need to encode the key because we know it is "src".
content.Append(" ")
.Append(srcKey)
.Append("=\\\"")
.Append(WebUtility.HtmlEncode(FallbackSrc))
.Append(WebUtility.HtmlEncode(srcValue))
.Append("\\\"");
}
}

View File

@ -39,6 +39,22 @@
<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,h=e.defaultView&&e.defaultView.getComputedStyle?e.defaultView.getComputedStyle(g):g.currentStyle;if(h&&h[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}("visibility","hidden",["\/site.css"]);</script>
<!-- Fallback from globbed href to static href -->
<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 from globbed href with exclude to static href -->
<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 from globbed and static href to static href -->
<link href="site.min.css" rel="stylesheet" data-extra="test" /><link href="/site.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 from globbed and static href with exclude 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,h=e.defaultView&&e.defaultView.getComputedStyle?e.defaultView.getComputedStyle(g):g.currentStyle;if(h&&h[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}("visibility","hidden",["\/site.css"]);</script>
@ -59,6 +75,26 @@
<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,h=e.defaultView&&e.defaultView.getComputedStyle?e.defaultView.getComputedStyle(g):g.currentStyle;if(h&&h[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 from globbed href to glbobed href -->
<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 from globbed href with exclude to globbed href -->
<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 from globbed and static href to globbed href -->
<link href="site.min.css" rel="stylesheet" data-extra="test" /><link href="/site.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 from globbed and static href with exclude 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>
<!-- Kitchen sink, all the attributes -->
<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" />

View File

@ -1,4 +1,5 @@
<!doctype html>

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
@ -6,22 +7,90 @@
</head>
<body>
<h2>Script tag helper test</h2>
<script src="/blank.js" data-foo="foo-data1">
<script src="/site.js" data-foo="foo-data1">
// Regular script with comment in body, and extra properties.
</script>
<script src="/blank.js" data-foo="foo-data2">
// TagHelper script with comment in body, and extra properties.
</script>
<script>(false||document.write("<script src=\"/fallback.js\" data-foo=\"foo-data2\"><\/script>"));</script>
<script>(false||document.write("<script src=\"/site.js\" data-foo=\"foo-data2\"><\/script>"));</script>
<script src="/blank.js">
// Fallback to globbed src
</script>
<script>(false||document.write("<script src=\"/site.js\"><\/script>"));</script>
<script src="/blank.js">
// Fallback to globbed src with exclude
</script>
<script>(false||document.write("<script src=\"/site.js\"><\/script><script src=\"/sub/site2.js\"><\/script>"));</script>
<script src="/blank.js">
// Fallback to globbed and static src
</script>
<script>(false||document.write("<script src=\"/site.js\"><\/script><script src=\"/sub/site2.js\"><\/script>"));</script>
<script src="/blank.js">
// Fallback to globbed and static src should de-dupe
</script>
<script>(false||document.write("<script src=\"/site.js\"><\/script>"));</script>
<script src="/blank.js">
// Fallback to globbed src with missing include
</script>
<script src="/blank.js">
// Fallback to static and globbed src with missing include
</script>
<script>(false||document.write("<script src=\"/site.js\"><\/script>"));</script>
<script src="/blank.js">
// Fallback to globbed src outside of webroot
</script>
<script src="/blank.js">
// Fallback to globbed src outside of webroot
</script>
<script data-foo="foo-data3">
// Valid TagHelper (although no src is provided) script with comment in body, and extra properties.
</script>
<script>(false||document.write("<script src=\"/fallback.js\" data-foo=\"foo-data3\"><\/script>"));</script>
<script>(false||document.write("<script src=\"/site.js\" data-foo=\"foo-data3\"><\/script>"));</script>
<script src="/blank.js">
// Invalid TagHelper script with comment in body.
</script>
<!-- Globbed script tag with existing file -->
<script src="/site.js"></script>
<!-- Globbed script tag with existing file and exclude -->
<script src="/site.js"></script><script src="/sub/site2.js"></script>
<script>
// Globbed script tag missing include
</script>
<script src="/site.js">
// Globbed script tag missing include but with static src
</script>
<!-- Globbed script tag with missing file -->
<!-- Globbed script tag with file outside of webroot -->
<!-- Globbed script tag with file outside of webroot -->
<script src="/site.js">
// Globbed script tag with existing file and static src
</script><script src="/sub/site2.js"></script>
<script src="/site.js">
// Globbed script tag with existing file and static src should dedupe
</script>
</body>
</html>

View File

@ -351,20 +351,16 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
return new TagHelperOutput(tagName, attributes);
}
private static IHostingEnvironment MakeHostingEnvironment(IFileProvider webRootFileProvider = null)
private static IHostingEnvironment MakeHostingEnvironment()
{
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 mockFileProvider = new Mock<IFileProvider>();
mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny<string>()))
.Returns(emptyDirectoryContents.Object);
var hostingEnvironment = new Mock<IHostingEnvironment>();
hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(webRootFileProvider);
hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object);
return hostingEnvironment.Object;
}

View File

@ -6,6 +6,8 @@ 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;
@ -20,39 +22,112 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
public class ScriptTagHelperTest
{
public static TheoryData RunsWhenRequiredAttributesArePresent_Data
{
get
{
return new TheoryData<IDictionary<string, object>, Action<ScriptTagHelper>>
{
{
new Dictionary<string, object>
{
["asp-src-include"] = "*.js"
},
tagHelper =>
{
tagHelper.SrcInclude = "*.js";
}
},
{
new Dictionary<string, object>
{
["asp-src-include"] = "*.js",
["asp-src-exclude"] = "*.min.js"
},
tagHelper =>
{
tagHelper.SrcInclude = "*.js";
tagHelper.SrcExclude = "*.min.js";
}
},
{
new Dictionary<string, object>
{
["asp-fallback-src"] = "test.js",
["asp-fallback-test"] = "isavailable()"
},
tagHelper =>
{
tagHelper.FallbackSrc = "test.js";
tagHelper.FallbackTestExpression = "isavailable()";
}
},
{
new Dictionary<string, object>
{
["asp-fallback-src-include"] = "*.js",
["asp-fallback-test"] = "isavailable()"
},
tagHelper =>
{
tagHelper.FallbackSrcInclude = "*.css";
tagHelper.FallbackTestExpression = "isavailable()";
}
},
{
new Dictionary<string, object>
{
["asp-fallback-src"] = "test.js",
["asp-fallback-src-include"] = "*.js",
["asp-fallback-test"] = "isavailable()"
},
tagHelper =>
{
tagHelper.FallbackSrc = "test.js";
tagHelper.FallbackSrcInclude = "*.css";
tagHelper.FallbackTestExpression = "isavailable()";
}
},
{
new Dictionary<string, object>
{
["asp-fallback-src-include"] = "*.js",
["asp-fallback-src-exclude"] = "*.min.js",
["asp-fallback-test"] = "isavailable()"
},
tagHelper =>
{
tagHelper.FallbackSrcInclude = "*.css";
tagHelper.FallbackSrcExclude = "*.min.css";
tagHelper.FallbackTestExpression = "isavailable()";
}
}
};
}
}
[Theory]
[InlineData("~/blank.js")]
[InlineData(null)]
public async Task RunsWhenRequiredAttributesArePresent(string srcValue)
[MemberData(nameof(RunsWhenRequiredAttributesArePresent_Data))]
public async Task RunsWhenRequiredAttributesArePresent(
IDictionary<string, object> attributes,
Action<ScriptTagHelper> setProperties)
{
// Arrange
var attributes = new Dictionary<string, object>
{
["asp-fallback-src"] = "http://www.example.com/blank.js",
["asp-fallback-test"] = "isavailable()",
};
if (srcValue != null)
{
attributes.Add("src", srcValue);
}
var tagHelperContext = MakeTagHelperContext(attributes);
var viewContext = MakeViewContext();
var context = MakeTagHelperContext(attributes);
var output = MakeTagHelperOutput("script");
var logger = CreateLogger();
var helper = new ScriptTagHelper()
var hostingEnvironment = MakeHostingEnvironment();
var viewContext = MakeViewContext();
var helper = new ScriptTagHelper
{
Logger = logger,
HostingEnvironment = hostingEnvironment,
ViewContext = viewContext,
FallbackSrc = "http://www.example.com/blank.js",
FallbackTestExpression = "isavailable()",
};
setProperties(helper);
// Act
await helper.ProcessAsync(tagHelperContext, output);
await helper.ProcessAsync(context, output);
// Assert
Assert.Null(output.TagName);
@ -61,57 +136,113 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Empty(logger.Logged);
}
public static TheoryData MissingAttributeDataSet
public static TheoryData DoesNotRunWhenARequiredAttributeIsMissing_Data
{
get
{
return new TheoryData<Dictionary<string, object>, ScriptTagHelper, string>
return new TheoryData<IDictionary<string, object>, Action<ScriptTagHelper>>
{
{
new Dictionary<string, object> // the attributes provided
new Dictionary<string, object>
{
["asp-fallback-src"] = "http://www.example.com/blank.js",
// This is commented out on purpose: ["asp-src-include"] = "*.js",
["asp-src-exclude"] = "*.min.js"
},
new ScriptTagHelper() // the tag helper
tagHelper =>
{
FallbackTestExpression = "isavailable()",
},
"asp-fallback-test" // missing attribute
// This is commented out on purpose: tagHelper.SrcInclude = "*.js";
tagHelper.SrcExclude = "*.min.js";
}
},
{
new Dictionary<string, object> // the attributes provided
new Dictionary<string, object>
{
// This is commented out on purpose: ["asp-fallback-src"] = "test.js",
["asp-fallback-test"] = "isavailable()",
},
new ScriptTagHelper() // the tag helper
tagHelper =>
{
FallbackTestExpression = "http://www.example.com/blank.js",
},
"asp-fallback-src" // missing attribute
// This is commented out on purpose: tagHelper.FallbackSrc = "test.js";
tagHelper.FallbackTestExpression = "isavailable()";
}
},
{
new Dictionary<string, object>
{
["asp-fallback-src"] = "test.js",
// This is commented out on purpose: ["asp-fallback-test"] = "isavailable()"
},
tagHelper =>
{
tagHelper.FallbackSrc = "test.js";
// This is commented out on purpose: tagHelper.FallbackTestExpression = "isavailable()";
}
},
{
new Dictionary<string, object>
{
// This is commented out on purpose: ["asp-fallback-src-include"] = "test.js",
["asp-fallback-src-exclude"] = "**/*.min.js",
["asp-fallback-test"] = "isavailable()",
},
tagHelper =>
{
// This is commented out on purpose: tagHelper.FallbackSrcInclude = "test.js";
tagHelper.FallbackSrcExclude = "**/*.min.js";
tagHelper.FallbackTestExpression = "isavailable()";
}
}
};
}
}
[Theory]
[MemberData(nameof(MissingAttributeDataSet))]
public async Task DoesNotRunWhenARequiredAttributeIsMissing(
Dictionary<string, object> attributes,
ScriptTagHelper helper,
string attributeMissing)
[MemberData(nameof(DoesNotRunWhenARequiredAttributeIsMissing_Data))]
public void DoesNotRunWhenARequiredAttributeIsMissing(
IDictionary<string, object> attributes,
Action<ScriptTagHelper> setProperties)
{
// Arrange
Assert.Single(attributes);
var tagHelperContext = MakeTagHelperContext(attributes);
var output = MakeTagHelperOutput("script");
var logger = new Mock<ILogger<ScriptTagHelper>>();
var hostingEnvironment = MakeHostingEnvironment();
var viewContext = MakeViewContext();
var helper = new ScriptTagHelper
{
Logger = logger.Object,
HostingEnvironment = hostingEnvironment,
ViewContext = viewContext
};
setProperties(helper);
// Act
helper.Process(tagHelperContext, output);
// Assert
Assert.NotNull(output.TagName);
Assert.False(output.ContentSet);
}
[Theory]
[MemberData(nameof(DoesNotRunWhenARequiredAttributeIsMissing_Data))]
public async Task LogsWhenARequiredAttributeIsMissing(
IDictionary<string, object> attributes,
Action<ScriptTagHelper> setProperties)
{
// Arrange
var tagHelperContext = MakeTagHelperContext(attributes);
var output = MakeTagHelperOutput("script");
var logger = CreateLogger();
helper.Logger = logger;
helper.ViewContext = viewContext;
var hostingEnvironment = MakeHostingEnvironment();
var viewContext = MakeViewContext();
var helper = new ScriptTagHelper
{
Logger = logger,
HostingEnvironment = hostingEnvironment,
ViewContext = viewContext
};
setProperties(helper);
// Act
await helper.ProcessAsync(tagHelperContext, output);
@ -119,6 +250,18 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Assert
Assert.Equal("script", output.TagName);
Assert.False(output.ContentSet);
Assert.Equal(2, logger.Logged.Count);
Assert.Equal(LogLevel.Warning, logger.Logged[0].LogLevel);
Assert.IsAssignableFrom<PartialModeMatchLoggerStructure>(logger.Logged[0].State);
var loggerData0 = (PartialModeMatchLoggerStructure)logger.Logged[0].State;
Assert.Equal(LogLevel.Verbose, logger.Logged[1].LogLevel);
Assert.IsAssignableFrom<ILoggerStructure>(logger.Logged[1].State);
Assert.StartsWith("Skipping processing for Microsoft.AspNet.Mvc.TagHelpers.ScriptTagHelper",
((ILoggerStructure)logger.Logged[1].State).Format());
}
[Fact]
@ -144,47 +287,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.False(output.ContentSet);
}
[Theory]
[MemberData(nameof(MissingAttributeDataSet))]
public async Task LogsWhenARequiredAttributeIsMissing(
Dictionary<string, object> attributes,
ScriptTagHelper helper,
string attributeMissing)
{
// Arrange
Assert.Single(attributes);
var tagHelperContext = MakeTagHelperContext(attributes);
var viewContext = MakeViewContext();
var output = MakeTagHelperOutput("script");
var logger = CreateLogger();
helper.Logger = logger;
helper.ViewContext = viewContext;
// Act
await helper.ProcessAsync(tagHelperContext, output);
// Assert
Assert.Equal("script", output.TagName);
Assert.False(output.ContentSet);
Assert.Equal(2, logger.Logged.Count);
Assert.Equal(LogLevel.Warning, logger.Logged[0].LogLevel);
Assert.IsType<MissingAttributeLoggerStructure>(logger.Logged[0].State);
var loggerData0 = (MissingAttributeLoggerStructure)logger.Logged[0].State;
Assert.Single(loggerData0.MissingAttributes);
Assert.Equal(attributeMissing, loggerData0.MissingAttributes.Single());
Assert.Equal(LogLevel.Verbose, logger.Logged[1].LogLevel);
Assert.IsAssignableFrom<ILoggerStructure>(logger.Logged[1].State);
Assert.StartsWith("Skipping processing for ScriptTagHelper",
((ILoggerStructure)logger.Logged[1].State).Format());
}
[Fact]
public async Task LogsWhenAllRequiredAttributesAreMissing()
{
@ -211,7 +313,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(LogLevel.Verbose, logger.Logged[0].LogLevel);
Assert.IsAssignableFrom<ILoggerStructure>(logger.Logged[0].State);
Assert.StartsWith("Skipping processing for ScriptTagHelper",
Assert.StartsWith("Skipping processing for Microsoft.AspNet.Mvc.TagHelpers.ScriptTagHelper",
((ILoggerStructure)logger.Logged[0].State).Format());
}
@ -231,7 +333,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var viewContext = MakeViewContext();
var output = MakeTagHelperOutput("link",
var output = MakeTagHelperOutput("src",
attributes: new Dictionary<string, string>
{
["data-extra"] = "something",
@ -240,11 +342,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
});
var logger = CreateLogger();
var hostingEnvironment = MakeHostingEnvironment();
var helper = new ScriptTagHelper
{
Logger = logger,
ViewContext = viewContext,
HostingEnvironment = hostingEnvironment,
FallbackSrc = "~/blank.js",
FallbackTestExpression = "http://www.example.com/blank.js",
};
@ -257,6 +361,42 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Empty(logger.Logged);
}
[Fact]
public async Task RendersScriptTagsForGlobbedSrcResults()
{
// Arrange
var context = MakeTagHelperContext(
attributes: new Dictionary<string, object>
{
["src"] = "/js/site.js",
["asp-src-include"] = "**/*.js"
});
var output = MakeTagHelperOutput("script", attributes: new Dictionary<string, string>
{
["src"] = "/js/site.js"
});
var logger = new Mock<ILogger<ScriptTagHelper>>();
var hostingEnvironment = MakeHostingEnvironment();
var viewContext = MakeViewContext();
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
globbingUrlBuilder.Setup(g => g.BuildUrlList("/js/site.js", "**/*.js", null))
.Returns(new[] { "/js/site.js", "/common.js" });
var helper = new ScriptTagHelper
{
GlobbingUrlBuilder = globbingUrlBuilder.Object,
Logger = logger.Object,
HostingEnvironment = hostingEnvironment,
ViewContext = viewContext,
SrcInclude = "**/*.js"
};
// Act
await helper.ProcessAsync(context, output);
// Assert
Assert.Equal("<script src=\"/js/site.js\"></script><script src=\"/common.js\"></script>", output.Content);
}
private TagHelperContext MakeTagHelperContext(
IDictionary<string, object> attributes = null,
string content = null)
@ -291,5 +431,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
return new TagHelperLogger<ScriptTagHelper>();
}
private static IHostingEnvironment MakeHostingEnvironment()
{
var emptyDirectoryContents = new Mock<IDirectoryContents>();
emptyDirectoryContents.Setup(dc => dc.GetEnumerator())
.Returns(Enumerable.Empty<IFileInfo>().GetEnumerator());
var mockFileProvider = new Mock<IFileProvider>();
mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny<string>()))
.Returns(emptyDirectoryContents.Object);
var hostingEnvironment = new Mock<IHostingEnvironment>();
hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object);
return hostingEnvironment.Object;
}
}
}

View File

@ -13,7 +13,7 @@
<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" />
<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" />
@ -43,6 +43,35 @@
asp-fallback-test-property="visibility"
asp-fallback-test-value="hidden" />
<!-- Fallback from globbed href to static href -->
<link asp-href-include="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 from globbed href with exclude to static href -->
<link asp-href-include="**/*.min.css" asp-href-exclude="**/site3.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 from globbed and static href to static href -->
<link href="site.min.css" asp-href-include="site.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 from globbed and static href with exclude to static href -->
<link href="site.min.css" asp-href-include="**/*.min.css" asp-href-exclude="**/site3.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"
@ -77,7 +106,46 @@
<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-href-exclude="**/site3*.css"
asp-fallback-test-class="hidden"
asp-fallback-test-property="visibility"
asp-fallback-test-value="hidden" />
<!-- Fallback from globbed href to glbobed href -->
<link asp-href-include="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 from globbed href with exclude to globbed href -->
<link asp-href-include="**/*.min.css" asp-href-exclude="**/site3.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 from globbed and static href to globbed href -->
<link href="site.min.css" asp-href-include="site.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 from globbed and static href with exclude to globbed href -->
<link href="site.min.css" asp-href-include="**/*.min.css" asp-href-exclude="**/site3.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" />
<!-- Kitchen sink, all the attributes -->
<link href="site.min.css" asp-href-include="**/*.min.css" asp-href-exclude="**/site3.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" />

View File

@ -11,20 +11,99 @@
</head>
<body>
<h2>Script tag helper test</h2>
<script src="~/blank.js" data-foo="foo-data1">
<script src="~/site.js" data-foo="foo-data1">
// Regular script with comment in body, and extra properties.
</script>
<script src="~/blank.js" asp-fallback-src="~/fallback.js" asp-fallback-test="false" data-foo="foo-data2">
<script src="~/blank.js" asp-fallback-src="~/site.js" asp-fallback-test="false" data-foo="foo-data2">
// TagHelper script with comment in body, and extra properties.
</script>
<script asp-fallback-src="~/fallback.js" asp-fallback-test="false" data-foo="foo-data3">
<script src="~/blank.js" asp-fallback-src-include="**/site.js" asp-fallback-test="false">
// Fallback to globbed src
</script>
<script src="~/blank.js"
asp-fallback-src-include="**/*.js"
asp-fallback-src-exclude="**/site3.js"
asp-fallback-test="false">
// Fallback to globbed src with exclude
</script>
<script src="~/blank.js"
asp-fallback-src="~/site.js"
asp-fallback-src-include="**/site2.js"
asp-fallback-test="false">
// Fallback to globbed and static src
</script>
<script src="~/blank.js"
asp-fallback-src="~/site.js"
asp-fallback-src-include="**/site.js"
asp-fallback-test="false">
// Fallback to globbed and static src should de-dupe
</script>
<script src="~/blank.js" asp-fallback-src-exclude="**/site3.js" asp-fallback-test="false">
// Fallback to globbed src with missing include
</script>
<script src="~/blank.js"
asp-fallback-src="~/site.js"
asp-fallback-src-exclude="**/site3.js"
asp-fallback-test="false">
// Fallback to static and globbed src with missing include
</script>
<script src="~/blank.js" asp-fallback-src-include="**/appRoot.js" asp-fallback-test="false">
// Fallback to globbed src outside of webroot
</script>
<script src="~/blank.js" asp-fallback-src-include="../**/appRoot.js" asp-fallback-test="false">
// Fallback to globbed src outside of webroot
</script>
<script asp-fallback-src="~/site.js" asp-fallback-test="false" data-foo="foo-data3">
// Valid TagHelper (although no src is provided) script with comment in body, and extra properties.
</script>
<script src="~/blank.js" asp-fallback-src="~/fallback.js">
<script src="~/blank.js" asp-fallback-src="~/site.js">
// Invalid TagHelper script with comment in body.
</script>
<!-- Globbed script tag with existing file -->
<script asp-src-include="**/site.js">
// This comment shouldn't be emitted
</script>
<!-- Globbed script tag with existing file and exclude -->
<script asp-src-include="**/*.js" asp-src-exclude="**/site3.js">
// This comment shouldn't be emitted
</script>
<script asp-src-exclude="**/site2.js">
// Globbed script tag missing include
</script>
<script src="~/site.js" asp-src-exclude="**/site2.js">
// Globbed script tag missing include but with static src
</script>
<!-- Globbed script tag with missing file -->
<script asp-src-include="**/notThere.js"></script>
<!-- Globbed script tag with file outside of webroot -->
<script asp-src-include="../**/appRoot.js"></script>
<!-- Globbed script tag with file outside of webroot -->
<script asp-src-include="**/appRoot.js"></script>
<script src="~/site.js" asp-src-include="**/site2.js">
// Globbed script tag with existing file and static src
</script>
<script src="~/site.js" asp-src-include="**/site.js">
// Globbed script tag with existing file and static src should dedupe
</script>
</body>
</html>

View File

@ -0,0 +1 @@
alert("ERROR!! This should never be loaded");

View File

@ -0,0 +1,6 @@
(function () {
var p = document.createElement("P");
p.appendChild(document.createTextNode("site.js loaded successfully!"));
p.style.color = "#0fa912";
document.body.appendChild(p);
})();

View File

@ -0,0 +1,6 @@
(function () {
var p = document.createElement("P");
p.appendChild(document.createTextNode("site2.js loaded successfully!"));
p.style.color = "#0fa912";
document.body.appendChild(p);
})();

View File

@ -0,0 +1 @@
alert("ERROR!! This should never be loaded");

View File

@ -0,0 +1,7 @@
body::after {
display: block;
background-color: #ff0000;
color: #fff;
margin-top: 2.4em;
content: "ERROR: Stylesheet 'site3.min.css' was loaded despite being excluded!";
}