diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs index 1ec061f64b..0bd0450735 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/UrlResolutionTagHelper.cs @@ -176,7 +176,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers string resolvedUrl; if (TryResolveUrl(stringValue, resolvedUrl: out resolvedUrl)) { - output.Attributes[i] = new TagHelperAttribute(attribute.Name, resolvedUrl); + output.Attributes[i] = new TagHelperAttribute( + attribute.Name, + resolvedUrl, + attribute.ValueStyle); } } else @@ -202,12 +205,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers IHtmlContent resolvedUrl; if (TryResolveUrl(stringValue, resolvedUrl: out resolvedUrl)) { - output.Attributes[i] = new TagHelperAttribute(attribute.Name, resolvedUrl); + output.Attributes[i] = new TagHelperAttribute( + attribute.Name, + resolvedUrl, + attribute.ValueStyle); } else if (htmlString == null) { // Not a ~/ URL. Just avoid re-encoding the attribute value later. - output.Attributes[i] = new TagHelperAttribute(attribute.Name, new HtmlString(stringValue)); + output.Attributes[i] = new TagHelperAttribute( + attribute.Name, + new HtmlString(stringValue), + attribute.ValueStyle); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs index 6d484b495e..f25bd690ae 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.IO; using System.Text; using System.Text.Encodings.Web; @@ -94,8 +93,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers try { - // The entry is set instead of assigning a value to the - // task so that the expiration options are are not impacted + // The entry is set instead of assigning a value to the + // task so that the expiration options are are not impacted // by the time it took to compute it. using (var entry = MemoryCache.CreateEntry(cacheKey)) @@ -118,7 +117,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } finally { - // If an exception occurs, ensure the other awaiters + // If an exception occurs, ensure the other awaiters // render the output by themselves. tcs.SetResult(null); } diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs index 72ee591204..5940e672cc 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs @@ -268,9 +268,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers if (Href != null) { var index = output.Attributes.IndexOfName(HrefAttributeName); + var existingAttribute = output.Attributes[index]; output.Attributes[index] = new TagHelperAttribute( - HrefAttributeName, - _fileVersionProvider.AddFileVersionToPath(Href)); + existingAttribute.Name, + _fileVersionProvider.AddFileVersionToPath(Href), + existingAttribute.ValueStyle); } } diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs index afec945db1..d19a0d42f5 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs @@ -234,10 +234,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers if (Src != null) { var index = output.Attributes.IndexOfName(SrcAttributeName); + var existingAttribute = output.Attributes[index]; output.Attributes[index] = new TagHelperAttribute( - SrcAttributeName, + existingAttribute.Name, _fileVersionProvider.AddFileVersionToPath(Src), - output.Attributes[index].ValueStyle); + existingAttribute.ValueStyle); } } diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs index 89168a6497..f9cc053ae8 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs @@ -3,7 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Razor.TagHelpers; @@ -90,9 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers /// The to merge attributes from. /// Existing on the given /// are not overridden; "class" attributes are merged with spaces. - public static void MergeAttributes( - this TagHelperOutput tagHelperOutput, - TagBuilder tagBuilder) + public static void MergeAttributes(this TagHelperOutput tagHelperOutput, TagBuilder tagBuilder) { if (tagHelperOutput == null) { @@ -110,18 +112,18 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { tagHelperOutput.Attributes.Add(attribute.Key, attribute.Value); } - else if (attribute.Key.Equals("class", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(attribute.Key, "class", StringComparison.OrdinalIgnoreCase)) { TagHelperAttribute classAttribute; + var found = tagHelperOutput.Attributes.TryGetAttribute("class", out classAttribute); + Debug.Assert(found); - if (tagHelperOutput.Attributes.TryGetAttribute("class", out classAttribute)) - { - tagHelperOutput.Attributes.SetAttribute("class", classAttribute.Value + " " + attribute.Value); - } - else - { - tagHelperOutput.Attributes.Add("class", attribute.Value); - } + var newAttribute = new TagHelperAttribute( + classAttribute.Name, + new ClassAttributeHtmlContent(classAttribute.Value, attribute.Value), + classAttribute.ValueStyle); + + tagHelperOutput.Attributes.SetAttribute(newAttribute); } } } @@ -201,5 +203,63 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers return -1; } + + private class ClassAttributeHtmlContent : IHtmlContent + { + private readonly object _left; + private readonly string _right; + + public ClassAttributeHtmlContent(object left, string right) + { + _left = left; + _right = right; + } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + // Write out "{left} {right}" in the common nothing-empty case. + var wroteLeft = false; + if (_left != null) + { + var htmlContent = _left as IHtmlContent; + if (htmlContent != null) + { + // Ignore case where htmlContent is HtmlString.Empty. At worst, will add a leading space to the + // generated attribute value. + htmlContent.WriteTo(writer, encoder); + wroteLeft = true; + } + else + { + var stringValue = _left.ToString(); + if (!string.IsNullOrEmpty(stringValue)) + { + encoder.Encode(writer, stringValue); + wroteLeft = true; + } + } + } + + if (!string.IsNullOrEmpty(_right)) + { + if (wroteLeft) + { + writer.Write(' '); + } + + encoder.Encode(writer, _right); + } + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html index e3a28d41de..700c9b1e45 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html @@ -74,7 +74,7 @@ Female -