diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs index 2abec8a630..72ee591204 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Html; @@ -47,6 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers private const string FallbackTestValueAttributeName = "asp-fallback-test-value"; private const string AppendVersionAttributeName = "asp-append-version"; private const string HrefAttributeName = "href"; + private const string RelAttributeName = "rel"; private static readonly Func Compare = (a, b) => a - b; private FileVersionProvider _fileVersionProvider; @@ -90,6 +92,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers FallbackTestValueAttributeName }), }; + private StringWriter _stringWriter; /// /// Creates a new . @@ -210,6 +213,20 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers // Internal for ease of use when testing. protected internal GlobbingUrlBuilder GlobbingUrlBuilder { get; set; } + // Shared writer for determining the string content of a TagHelperAttribute's Value. + private StringWriter StringWriter + { + get + { + if (_stringWriter == null) + { + _stringWriter = new StringWriter(); + } + + return _stringWriter; + } + } + /// public override void Process(TagHelperContext context, TagHelperOutput output) { @@ -271,7 +288,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } } - if (mode == Mode.Fallback) + if (mode == Mode.Fallback && HasStyleSheetLinkType(output.Attributes)) { string resolvedUrl; if (TryResolveUrl(FallbackHref, resolvedUrl: out resolvedUrl)) @@ -279,7 +296,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers FallbackHref = resolvedUrl; } - BuildFallbackBlock(builder); + BuildFallbackBlock(output.Attributes, builder); } } @@ -306,7 +323,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } } - private void BuildFallbackBlock(TagHelperContent builder) + private void BuildFallbackBlock(TagHelperAttributeList attributes, TagHelperContent builder) { EnsureGlobbingUrlBuilder(); var fallbackHrefs = GlobbingUrlBuilder.BuildUrlList( @@ -341,7 +358,59 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers .AppendHtml("\","); AppendFallbackHrefs(builder, fallbackHrefs); - builder.AppendHtml(""); + + builder.AppendHtml(", \""); + + // Perf: Avoid allocating enumerator + for (var i = 0; i < attributes.Count; i++) + { + var attribute = attributes[i]; + if (string.Equals(attribute.Name, HrefAttributeName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + attribute.WriteTo(StringWriter, HtmlEncoder); + StringWriter.Write(' '); + } + + var stringBuilder = StringWriter.GetStringBuilder(); + var scriptTags = stringBuilder.ToString(); + stringBuilder.Clear(); + var encodedScriptTags = JavaScriptEncoder.Encode(scriptTags); + builder.AppendHtml(encodedScriptTags); + + builder.AppendHtml("\");"); + } + + private bool HasStyleSheetLinkType(TagHelperAttributeList attributes) + { + TagHelperAttribute relAttribute; + if (!attributes.TryGetAttribute(RelAttributeName, out relAttribute) || + relAttribute.Value == null) + { + return false; + } + + var attributeValue = relAttribute.Value; + var contentValue = attributeValue as IHtmlContent; + var stringValue = attributeValue as string; + if (contentValue != null) + { + contentValue.WriteTo(StringWriter, HtmlEncoder); + stringValue = StringWriter.ToString(); + + // Reset writer + StringWriter.GetStringBuilder().Clear(); + } + else if (stringValue == null) + { + stringValue = attributeValue.ToString(); + } + + var hasRelStylesheet = string.Equals("stylesheet", stringValue, StringComparison.Ordinal); + + return hasRelStylesheet; } private void AppendFallbackHrefs(TagHelperContent builder, IReadOnlyList fallbackHrefs) @@ -378,7 +447,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers builder.AppendHtml(valueToWrite); builder.AppendHtml("\""); } - builder.AppendHtml("]);"); + builder.AppendHtml("]"); } private void EnsureGlobbingUrlBuilder() diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/compiler/resources/LinkTagHelper_FallbackJavaScript.js b/src/Microsoft.AspNetCore.Mvc.TagHelpers/compiler/resources/LinkTagHelper_FallbackJavaScript.js index 8663692cf5..7d4596bfea 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/compiler/resources/LinkTagHelper_FallbackJavaScript.js +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/compiler/resources/LinkTagHelper_FallbackJavaScript.js @@ -1 +1 @@ -!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')}(); \ No newline at end of file +!function(a,b,c,d){var e,f=document,g=f.getElementsByTagName("SCRIPT"),h=g[g.length-1].previousElementSibling,i=f.defaultView&&f.defaultView.getComputedStyle?f.defaultView.getComputedStyle(h):h.currentStyle;if(i&&i[a]!==b)for(e=0;e")}(); \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js b/src/Microsoft.AspNetCore.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js index bee98a7af2..17e229dc49 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/js/LinkTagHelper_FallbackJavaScript.js @@ -8,9 +8,10 @@ * * @param {string} cssTestPropertyName - The name of the CSS property to test. * @param {string} cssTestPropertyValue - The value to test the specified CSS property for. - * @param {string[]} fallbackHref - The URLs to the stylesheets to load in the case the test fails. + * @param {string[]} fallbackHrefs - The URLs to the stylesheets to load in the case the test fails. + * @param {string} extraAttributes - The extra attributes string that should be included on the generated link tags. */ - function loadFallbackStylesheet(cssTestPropertyName, cssTestPropertyValue, fallbackHref) { + function loadFallbackStylesheet(cssTestPropertyName, cssTestPropertyValue, fallbackHrefs, extraAttributes) { var doc = document, // Find the last script tag on the page which will be this one, as JS executes as it loads scriptElements = doc.getElementsByTagName("SCRIPT"), @@ -22,8 +23,8 @@ i; if (metaStyle && metaStyle[cssTestPropertyName] !== cssTestPropertyValue) { - for (i = 0; i < fallbackHref.length; i++) { - doc.write(''); + for (i = 0; i < fallbackHrefs.length; i++) { + doc.write(''); } } })(); \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html index a5dac00b74..30f65f5ccd 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html @@ -39,63 +39,69 @@ - + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -120,7 +126,7 @@ - + diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html index 370692a400..cb7acce37c 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html @@ -39,63 +39,70 @@ - + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -120,7 +127,7 @@ - + diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/LinkTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/LinkTagHelperTest.cs index 092125b89c..5f047a3a1b 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/LinkTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/LinkTagHelperTest.cs @@ -785,14 +785,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { // Arrange var expectedPostElement = Environment.NewLine + - "" + - ""; + ""; var context = MakeTagHelperContext( attributes: new TagHelperAttributeList { @@ -802,8 +801,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { "asp-fallback-test-property", "visibility" }, { "asp-fallback-test-value", "hidden" }, { "href", "/css/site.css" }, + { "rel", new HtmlString("stylesheet") }, + }); + var output = MakeTagHelperOutput( + "link", + attributes: new TagHelperAttributeList + { + { "rel", new HtmlString("stylesheet") }, }); - var output = MakeTagHelperOutput("link", attributes: new TagHelperAttributeList()); var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); var globbingUrlBuilder = new Mock( @@ -846,15 +851,16 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers var expectedContent = "" + + "mixed=\"HtmlEncode[[HTML encoded]] and contains \"quotes\"\" rel=\"stylesheet\" />" + Environment.NewLine + - "" + - ""; var mixed = new DefaultTagHelperContent(); mixed.Append("HTML encoded"); @@ -871,6 +877,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { "href", "/css/site.css" }, { "literal", "all HTML encoded" }, { "mixed", mixed }, + { "rel", new HtmlString("stylesheet") }, }); var output = MakeTagHelperOutput( "link", @@ -879,6 +886,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { "encoded", new HtmlString("contains \"quotes\"") }, { "literal", "all HTML encoded" }, { "mixed", mixed }, + { "rel", new HtmlString("stylesheet") }, }); var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); diff --git a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Link.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Link.cshtml index 9c7a429eec..09f614154a 100644 --- a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Link.cshtml +++ b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Link.cshtml @@ -1,4 +1,4 @@ -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @@ -46,6 +46,20 @@ asp-fallback-test-property="visibility" asp-fallback-test-value="hidden" /> + + + + + +