Skip to content

Commit

Permalink
Preserve class casing when merging a TagBuilder into a `TagHelper…
Browse files Browse the repository at this point in the history
…Output`

- aspnet#5313

Also:
- preserve existing `TagHelperAttribute.ValueStyle`
 - fix this in `UrlResolutionTagHelper`, `LinkTagHelper`, and `ScriptTagHelper` as well
- correct handling of non-`string` `classAttribute.Value`s in `TagHelperOutputExtensions`
 - relates to aspnet#3918 because new `ClassAttributeHtmlContent` is smaller than any concatenated attribute value

nit: clean up `CacheTagHelper` whitespace and `using`s
  • Loading branch information
dougbu committed Sep 26, 2016
1 parent 0d782d9 commit 27e4822
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,10 @@ protected void ProcessUrlAttribute(string attributeName, TagHelperOutput output)
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
Expand All @@ -202,12 +205,18 @@ protected void ProcessUrlAttribute(string attributeName, TagHelperOutput output)
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);
}
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,8 +93,8 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu

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))
Expand All @@ -118,7 +117,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
}
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);
}
Expand Down
6 changes: 4 additions & 2 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,11 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
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);
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,11 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -90,9 +94,7 @@ public static void CopyHtmlAttribute(
/// <param name="tagBuilder">The <see cref="TagBuilder"/> to merge attributes from.</param>
/// <remarks>Existing <see cref="TagHelperOutput.Attributes"/> on the given <paramref name="tagHelperOutput"/>
/// are not overridden; "class" attributes are merged with spaces.</remarks>
public static void MergeAttributes(
this TagHelperOutput tagHelperOutput,
TagBuilder tagBuilder)
public static void MergeAttributes(this TagHelperOutput tagHelperOutput, TagBuilder tagBuilder)
{
if (tagHelperOutput == null)
{
Expand All @@ -110,18 +112,18 @@ public static void MergeAttributes(
{
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);
}
}
}
Expand Down Expand Up @@ -201,5 +203,63 @@ private static int IndexOfFirstMatch(string name, TagHelperAttributeList attribu

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);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
<input type="HtmlEncode[[radio]]" value="HtmlEncode[[Female]]" checked="HtmlEncode[[checked]]" id="HtmlEncode[[Customer_Gender]]" name="HtmlEncode[[Customer.Gender]]" /> Female
<span class="HtmlEncode[[field-validation-valid]]" data-valmsg-for="HtmlEncode[[Customer.Gender]]" data-valmsg-replace="HtmlEncode[[true]]"></span>
</div>
<div class="HtmlEncode[[order validation-summary-valid]]" data-valmsg-summary="HtmlEncode[[true]]"><ul><li style="display:none"></li>
<div class="order HtmlEncode[[validation-summary-valid]]" data-valmsg-summary="HtmlEncode[[true]]"><ul><li style="display:none"></li>
</ul></div>
<input type="HtmlEncode[[hidden]]" id="HtmlEncode[[Customer_Key]]" name="HtmlEncode[[Customer.Key]]" value="HtmlEncode[[KeyA]]" />
<input type="submit" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
public class CaseSensitiveTagHelperAttributeComparer : IEqualityComparer<TagHelperAttribute>
{
public readonly static CaseSensitiveTagHelperAttributeComparer Default =
new CaseSensitiveTagHelperAttributeComparer();

private CaseSensitiveTagHelperAttributeComparer()
{
}

public bool Equals(TagHelperAttribute attributeX, TagHelperAttribute attributeY)
{
if (attributeX == attributeY)
{
return true;
}

// Normal comparer (TagHelperAttribute.Equals()) doesn't care about the Name case, in tests we do.
return attributeX != null &&
string.Equals(attributeX.Name, attributeY.Name, StringComparison.Ordinal) &&
attributeX.ValueStyle == attributeY.ValueStyle &&
(attributeX.ValueStyle == HtmlAttributeValueStyle.Minimized ||
string.Equals(GetString(attributeX.Value), GetString(attributeY.Value)));
}

public int GetHashCode(TagHelperAttribute attribute)
{
// Manually combine hash codes here. We can't reference HashCodeCombiner because we have internals visible
// from Mvc.Core and Mvc.TagHelpers; both of which reference HashCodeCombiner.
var baseHashCode = 0x1505L;
var attributeHashCode = attribute.GetHashCode();
var combinedHash = ((baseHashCode << 5) + baseHashCode) ^ attributeHashCode;
var nameHashCode = StringComparer.Ordinal.GetHashCode(attribute.Name);
combinedHash = ((combinedHash << 5) + combinedHash) ^ nameHashCode;

return combinedHash.GetHashCode();
}

private string GetString(object value)
{
var htmlContent = value as IHtmlContent;
if (htmlContent != null)
{
using (var writer = new StringWriter())
{
htmlContent.WriteTo(writer, NullHtmlEncoder.Default);
return writer.ToString();
}
}

return value?.ToString() ?? string.Empty;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ public async Task ProcessAsync_CallsGenerateTextBox_WithExpectedParametersForHid
htmlGenerator.Verify();

Assert.Equal(TagMode.StartTagOnly, output.TagMode);
Assert.Equal(expectedAttributes, output.Attributes);
Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
Expand Down Expand Up @@ -590,7 +590,7 @@ public async Task ProcessAsync_CallsGeneratePassword_WithExpectedParameters(
htmlGenerator.Verify();

Assert.Equal(TagMode.StartTagOnly, output.TagMode);
Assert.Equal(expectedAttributes, output.Attributes);
Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
Expand Down Expand Up @@ -684,7 +684,7 @@ public async Task ProcessAsync_CallsGenerateRadioButton_WithExpectedParameters(
htmlGenerator.Verify();

Assert.Equal(TagMode.StartTagOnly, output.TagMode);
Assert.Equal(expectedAttributes, output.Attributes);
Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
Expand Down Expand Up @@ -801,7 +801,7 @@ public async Task ProcessAsync_CallsGenerateTextBox_WithExpectedParameters(
htmlGenerator.Verify();

Assert.Equal(TagMode.StartTagOnly, output.TagMode);
Assert.Equal(expectedAttributes, output.Attributes);
Assert.Equal(expectedAttributes, output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default);
Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, output.Content.GetContent());
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
Expand Down
Loading

0 comments on commit 27e4822

Please sign in to comment.