parent
6490b113d8
commit
8b64e3c7bb
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <link> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <script> 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("\\\"");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
alert("ERROR!! This should never be loaded");
|
||||
|
|
@ -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);
|
||||
})();
|
||||
|
|
@ -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);
|
||||
})();
|
||||
|
|
@ -0,0 +1 @@
|
|||
alert("ERROR!! This should never be loaded");
|
||||
|
|
@ -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!";
|
||||
}
|
||||
Loading…
Reference in New Issue