Skip to content
This repository has been archived by the owner on Dec 19, 2018. It is now read-only.

Commit

Permalink
Add CSS attribute selectors for TagHelper attributes.
Browse files Browse the repository at this point in the history
- Added the ability for users to opt into CSS `TagHelper` selectors in their required attributes by surrounding the value with `[` and `]`. Added operators `^`, `$` and `=`.
- Added tests to cover code paths used when determining CSS selectors.

#684
  • Loading branch information
NTaylorMullen committed Mar 7, 2016
1 parent 9bd06a5 commit a6be4d1
Show file tree
Hide file tree
Showing 19 changed files with 1,603 additions and 211 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ public override bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor
// attributes or prefixes. In tests we do.
Assert.Equal(descriptorX.TagName, descriptorY.TagName, StringComparer.Ordinal);
Assert.Equal(descriptorX.Prefix, descriptorY.Prefix, StringComparer.Ordinal);
Assert.Equal(descriptorX.RequiredAttributes, descriptorY.RequiredAttributes, StringComparer.Ordinal);
Assert.Equal(
descriptorX.RequiredAttributes,
descriptorY.RequiredAttributes,
CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.Default);
Assert.Equal(descriptorX.RequiredParent, descriptorY.RequiredParent, StringComparer.Ordinal);

if (descriptorX.AllowedChildren != descriptorY.AllowedChildren)
Expand Down Expand Up @@ -66,9 +69,10 @@ public override int GetHashCode(TagHelperDescriptor descriptor)
TagHelperDesignTimeDescriptorComparer.Default.GetHashCode(descriptor.DesignTimeDescriptor));
}

foreach (var requiredAttribute in descriptor.RequiredAttributes.OrderBy(attribute => attribute))
foreach (var requiredAttribute in descriptor.RequiredAttributes.OrderBy(attribute => attribute.Name))
{
hashCodeCombiner.Add(requiredAttribute, StringComparer.Ordinal);
hashCodeCombiner.Add(
CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.Default.GetHashCode(requiredAttribute));
}

if (descriptor.AllowedChildren != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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 Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
using Microsoft.Extensions.Internal;
using Xunit;

namespace Microsoft.AspNetCore.Razor.Test.Internal
{
internal class CaseSensitiveTagHelperRequiredAttributeDescriptorComparer : TagHelperRequiredAttributeDescriptorComparer
{
public new static readonly CaseSensitiveTagHelperRequiredAttributeDescriptorComparer Default =
new CaseSensitiveTagHelperRequiredAttributeDescriptorComparer();

private CaseSensitiveTagHelperRequiredAttributeDescriptorComparer()
: base()
{
}

public override bool Equals(TagHelperRequiredAttributeDescriptor descriptorX, TagHelperRequiredAttributeDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}

Assert.True(base.Equals(descriptorX, descriptorY));

Assert.Equal(descriptorX.Name, descriptorY.Name, StringComparer.Ordinal);

return true;
}

public override int GetHashCode(TagHelperRequiredAttributeDescriptor descriptor)
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(base.GetHashCode(descriptor));
hashCodeCombiner.Add(descriptor.Name, StringComparer.Ordinal);

return hashCodeCombiner.CombinedHash;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class TagHelperDescriptor
private string _assemblyName;
private IEnumerable<TagHelperAttributeDescriptor> _attributes =
Enumerable.Empty<TagHelperAttributeDescriptor>();
private IEnumerable<string> _requiredAttributes = Enumerable.Empty<string>();
private IEnumerable<TagHelperRequiredAttributeDescriptor> _requiredAttributes = Enumerable.Empty<TagHelperRequiredAttributeDescriptor>();

/// <summary>
/// Text used as a required prefix when matching HTML start and end tags in the Razor source to available
Expand Down Expand Up @@ -140,7 +140,7 @@ public IEnumerable<TagHelperAttributeDescriptor> Attributes
/// <remarks>
/// <c>*</c> at the end of an attribute name acts as a prefix match.
/// </remarks>
public IEnumerable<string> RequiredAttributes
public IEnumerable<TagHelperRequiredAttributeDescriptor> RequiredAttributes
{
get
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ public virtual bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor
descriptorY.RequiredParent,
StringComparison.OrdinalIgnoreCase) &&
Enumerable.SequenceEqual(
descriptorX.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase),
descriptorY.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase),
StringComparer.OrdinalIgnoreCase) &&
descriptorX.RequiredAttributes.OrderBy(attribute => attribute.Name, StringComparer.OrdinalIgnoreCase),
descriptorY.RequiredAttributes.OrderBy(attribute => attribute.Name, StringComparer.OrdinalIgnoreCase),
TagHelperRequiredAttributeDescriptorComparer.Default) &&
(descriptorX.AllowedChildren == descriptorY.AllowedChildren ||
(descriptorX.AllowedChildren != null &&
descriptorY.AllowedChildren != null &&
Expand All @@ -80,11 +80,11 @@ public virtual int GetHashCode(TagHelperDescriptor descriptor)
hashCodeCombiner.Add(descriptor.TagStructure);

var attributes = descriptor.RequiredAttributes.OrderBy(
attribute => attribute,
attribute => attribute.Name,
StringComparer.OrdinalIgnoreCase);
foreach (var attribute in attributes)
{
hashCodeCombiner.Add(attribute, StringComparer.OrdinalIgnoreCase);
hashCodeCombiner.Add(TagHelperRequiredAttributeDescriptorComparer.Default.GetHashCode(attribute));
}

if (descriptor.AllowedChildren != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Parser.TagHelpers;

namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
{
Expand All @@ -14,8 +15,6 @@ public class TagHelperDescriptorProvider
{
public const string ElementCatchAllTarget = "*";

public static readonly string RequiredAttributeWildcardSuffix = "*";

private IDictionary<string, HashSet<TagHelperDescriptor>> _registrations;
private string _tagHelperPrefix;

Expand All @@ -39,14 +38,14 @@ public TagHelperDescriptorProvider(IEnumerable<TagHelperDescriptor> descriptors)
/// </summary>
/// <param name="tagName">The name of the HTML tag to match. Providing a '*' tag name
/// retrieves catch-all <see cref="TagHelperDescriptor"/>s (descriptors that target every tag).</param>
/// <param name="attributeNames">Attributes the HTML element must contain to match.</param>
/// <param name="attributes">Attributes the HTML element must contain to match.</param>
/// <param name="parentTagName">The parent tag name of the given <paramref name="tagName"/> tag.</param>
/// <returns><see cref="TagHelperDescriptor"/>s that apply to the given <paramref name="tagName"/>.
/// Will return an empty <see cref="Enumerable" /> if no <see cref="TagHelperDescriptor"/>s are
/// found.</returns>
public IEnumerable<TagHelperDescriptor> GetDescriptors(
string tagName,
IEnumerable<string> attributeNames,
IEnumerable<KeyValuePair<string, string>> attributes,
string parentTagName)
{
if (!string.IsNullOrEmpty(_tagHelperPrefix) &&
Expand Down Expand Up @@ -78,10 +77,10 @@ public TagHelperDescriptorProvider(IEnumerable<TagHelperDescriptor> descriptors)
descriptors = matchingDescriptors.Concat(descriptors);
}

var applicableDescriptors = ApplyRequiredAttributes(descriptors, attributeNames);
var applicableDescriptors = ApplyRequiredAttributes(descriptors, attributes);
applicableDescriptors = ApplyParentTagFilter(applicableDescriptors, parentTagName);

return applicableDescriptors;
return applicableDescriptors.ToArray();
}

private IEnumerable<TagHelperDescriptor> ApplyParentTagFilter(
Expand All @@ -95,37 +94,12 @@ public TagHelperDescriptorProvider(IEnumerable<TagHelperDescriptor> descriptors)

private IEnumerable<TagHelperDescriptor> ApplyRequiredAttributes(
IEnumerable<TagHelperDescriptor> descriptors,
IEnumerable<string> attributeNames)
IEnumerable<KeyValuePair<string, string>> attributes)
{
return descriptors.Where(
descriptor =>
{
foreach (var requiredAttribute in descriptor.RequiredAttributes)
{
// '*' at the end of a required attribute indicates: apply to attributes prefixed with the
// required attribute value.
if (requiredAttribute.EndsWith(
RequiredAttributeWildcardSuffix,
StringComparison.OrdinalIgnoreCase))
{
var prefix = requiredAttribute.Substring(0, requiredAttribute.Length - 1);
if (!attributeNames.Any(
attributeName =>
attributeName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(attributeName, prefix, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
}
else if (!attributeNames.Contains(requiredAttribute, StringComparer.OrdinalIgnoreCase))
{
return false;
}
}
return true;
});
descriptor => descriptor.RequiredAttributes.All(
requiredAttribute => attributes.Any(
attribute => requiredAttribute.Matches(attribute.Key, attribute.Value))));
}

private void Register(TagHelperDescriptor descriptor)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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;

namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
{
/// <summary>
/// A metadata class describing a required tag helper attribute.
/// </summary>
public class TagHelperRequiredAttributeDescriptor
{
/// <summary>
/// The HTML attribute name.
/// </summary>
public string Name { get; set; }

/// <summary>
/// The HTML attribute selector value. If <see cref="IsCSSSelector"/> is not <c>true</c>, this field is
/// ignored.
/// </summary>
public string Value { get; set; }

/// <summary>
/// An operator that modifies how a required attribute is applied to an HTML attribute value or name.
/// </summary>
public char Operator { get; set; }

/// <summary>
/// Indicates if the <see cref="TagHelperRequiredAttributeDescriptor"/> represents a CSS selector.
/// </summary>
public bool IsCSSSelector { get; set; }

/// <summary>
/// Determines if the current <see cref="TagHelperRequiredAttributeDescriptor"/> matches the given
/// <paramref name="attributeName"/> and <paramref name="attributeValue"/>.
/// </summary>
/// <param name="attributeName">An HTML attribute name.</param>
/// <param name="attributeValue">An HTML attribute value.</param>
/// <returns></returns>
public bool Matches(string attributeName, string attributeValue)
{
if (IsCSSSelector)
{
var nameMatches = string.Equals(Name, attributeName, StringComparison.OrdinalIgnoreCase);

if (!nameMatches)
{
return false;
}

var valueMatches = false;
switch (Operator)
{
case '^': // Value starts with
valueMatches = attributeValue.StartsWith(Value, StringComparison.Ordinal);
break;
case '$': // Value ends with
valueMatches = attributeValue.EndsWith(Value, StringComparison.Ordinal);
break;
case '=': // Value equals
valueMatches = string.Equals(attributeValue, Value, StringComparison.Ordinal);
break;
default: // No value selector, force true because at least the attribute name matched.
valueMatches = true;
break;
}

return valueMatches;
}
else if (Operator == '*')
{
return attributeName.Length != Name.Length &
attributeName.StartsWith(Name, StringComparison.OrdinalIgnoreCase);
}
else
{
return string.Equals(Name, attributeName, StringComparison.OrdinalIgnoreCase);
}
}

/// <summary>
/// Determines whether the provided <paramref name="op"/> is a supported CSS value operator.
/// </summary>
/// <param name="op">The CSS value operator</param>
/// <returns><c>true</c> if <paramref name="op"/> is <c>=</c>, <c>^</c> or <c>$</c>; <c>false</c> otherwise.
/// </returns>
public static bool IsSupportedCSSValueOperator(char op)
{
return op == '=' || op == '^' || op == '$';
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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 Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
{
/// <summary>
/// An <see cref="IEqualityComparer{TagHelperRequiredAttributeDescriptor}"/> used to check equality between
/// two <see cref="TagHelperRequiredAttributeDescriptor"/>s.
/// </summary>
public class TagHelperRequiredAttributeDescriptorComparer : IEqualityComparer<TagHelperRequiredAttributeDescriptor>
{
/// <summary>
/// A default instance of the <see cref="TagHelperRequiredAttributeDescriptor"/>.
/// </summary>
public static readonly TagHelperRequiredAttributeDescriptorComparer Default = new TagHelperRequiredAttributeDescriptorComparer();

/// <summary>
/// Initializes a new <see cref="TagHelperRequiredAttributeDescriptor"/> instance.
/// </summary>
protected TagHelperRequiredAttributeDescriptorComparer()
{
}

/// <inheritdoc />
public virtual bool Equals(TagHelperRequiredAttributeDescriptor descriptorX, TagHelperRequiredAttributeDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}

return descriptorX != null &&
descriptorX.Operator == descriptorY.Operator &&
descriptorX.IsCSSSelector == descriptorY.IsCSSSelector &&
string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.OrdinalIgnoreCase) &&
string.Equals(descriptorX.Value, descriptorY.Value, StringComparison.Ordinal);
}

/// <inheritdoc />
public virtual int GetHashCode(TagHelperRequiredAttributeDescriptor descriptor)
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(descriptor.Operator);
hashCodeCombiner.Add(descriptor.IsCSSSelector);
hashCodeCombiner.Add(descriptor.Name, StringComparer.OrdinalIgnoreCase);
hashCodeCombiner.Add(descriptor.Value, StringComparer.Ordinal);

return hashCodeCombiner.CombinedHash;
}
}
}
2 changes: 1 addition & 1 deletion src/Microsoft.AspNetCore.Razor/Parser/RazorParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ private ParserResults ParseCore(ITextDocument input)
return addOrRemoveTagHelperSpanVisitor.GetDescriptors(documentRoot);
}

private static IEnumerable<ISyntaxTreeRewriter> GetDefaultRewriters(ParserBase markupParser)
internal static IEnumerable<ISyntaxTreeRewriter> GetDefaultRewriters(ParserBase markupParser)
{
return new ISyntaxTreeRewriter[]
{
Expand Down
Loading

0 comments on commit a6be4d1

Please sign in to comment.