Add CSS attribute selectors for `TagHelper` attributes.

- 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
This commit is contained in:
N. Taylor Mullen 2016-02-13 13:59:51 -08:00
parent 3b53e42f36
commit e5927ddd01
24 changed files with 1851 additions and 215 deletions

View File

@ -426,6 +426,86 @@ namespace Microsoft.AspNetCore.Razor.Runtime
return string.Format(CultureInfo.CurrentCulture, GetString("ArgumentMustBeAnInstanceOf"), p0);
}
/// <summary>
/// Could not find matching ']' for required attribute '{0}'.
/// </summary>
internal static string TagHelperDescriptorFactory_CouldNotFindMatchingEndBrace
{
get { return GetString("TagHelperDescriptorFactory_CouldNotFindMatchingEndBrace"); }
}
/// <summary>
/// Could not find matching ']' for required attribute '{0}'.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_CouldNotFindMatchingEndBrace"), p0);
}
/// <summary>
/// Invalid required attribute character '{0}' in required attribute '{1}'. Separate required attributes with commas.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidRequiredAttributeCharacter
{
get { return GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeCharacter"); }
}
/// <summary>
/// Invalid required attribute character '{0}' in required attribute '{1}'. Separate required attributes with commas.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeCharacter"), p0, p1);
}
/// <summary>
/// Required attribute '{0}' has mismatched quotes '{1}' around value.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes
{
get { return GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes"); }
}
/// <summary>
/// Required attribute '{0}' has mismatched quotes '{1}' around value.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes"), p0, p1);
}
/// <summary>
/// Required attribute '{0}' has a partial CSS operator. '{1}' must be followed by an equals.
/// </summary>
internal static string TagHelperDescriptorFactory_PartialRequiredAttributeOperator
{
get { return GetString("TagHelperDescriptorFactory_PartialRequiredAttributeOperator"); }
}
/// <summary>
/// Required attribute '{0}' has a partial CSS operator. '{1}' must be followed by an equals.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_PartialRequiredAttributeOperator(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_PartialRequiredAttributeOperator"), p0, p1);
}
/// <summary>
/// Invalid character '{0}' in required attribute '{1}'. Expected supported CSS operator or ']'.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidRequiredAttributeOperator
{
get { return GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeOperator"); }
}
/// <summary>
/// Invalid character '{0}' in required attribute '{1}'. Expected supported CSS operator or ']'.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidRequiredAttributeOperator(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeOperator"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -195,4 +195,19 @@
<data name="ArgumentMustBeAnInstanceOf" xml:space="preserve">
<value>Argument must be an instance of '{0}'.</value>
</data>
<data name="TagHelperDescriptorFactory_CouldNotFindMatchingEndBrace" xml:space="preserve">
<value>Could not find matching ']' for required attribute '{0}'.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidRequiredAttributeCharacter" xml:space="preserve">
<value>Invalid required attribute character '{0}' in required attribute '{1}'. Separate required attributes with commas.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes" xml:space="preserve">
<value>Required attribute '{0}' has mismatched quotes '{1}' around value.</value>
</data>
<data name="TagHelperDescriptorFactory_PartialRequiredAttributeOperator" xml:space="preserve">
<value>Required attribute '{0}' has a partial CSS operator. '{1}' must be followed by an equals.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidRequiredAttributeOperator" xml:space="preserve">
<value>Invalid character '{0}' in required attribute '{1}'. Expected supported CSS operator or ']'.</value>
</data>
</root>

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
@ -21,6 +22,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
private const string DataDashPrefix = "data-";
private const string TagHelperNameEnding = "TagHelper";
private const string HtmlCaseRegexReplacement = "-$1$2";
private const char RequiredAttributeWildcardSuffix = '*';
// This matches the following AFTER the start of the input string (MATCH).
// Any letter/number followed by an uppercase letter then lowercase letter: 1(Aa), a(Aa), A(Aa)
@ -153,7 +155,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeName,
assemblyName,
attributeDescriptors,
requiredAttributes: Enumerable.Empty<string>(),
requiredAttributeDescriptors: Enumerable.Empty<TagHelperRequiredAttributeDescriptor>(),
allowedChildren: allowedChildren,
tagStructure: default(TagStructure),
parentTag: null,
@ -235,14 +237,15 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
IEnumerable<string> allowedChildren,
TagHelperDesignTimeDescriptor designTimeDescriptor)
{
var requiredAttributes = GetCommaSeparatedValues(targetElementAttribute.Attributes);
IEnumerable<TagHelperRequiredAttributeDescriptor> requiredAttributeDescriptors;
TryGetRequiredAttributeDescriptors(targetElementAttribute.Attributes, errorSink: null, descriptors: out requiredAttributeDescriptors);
return BuildTagHelperDescriptor(
targetElementAttribute.Tag,
typeName,
assemblyName,
attributeDescriptors,
requiredAttributes,
requiredAttributeDescriptors,
allowedChildren,
targetElementAttribute.ParentTag,
targetElementAttribute.TagStructure,
@ -254,7 +257,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
string typeName,
string assemblyName,
IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,
IEnumerable<string> requiredAttributes,
IEnumerable<TagHelperRequiredAttributeDescriptor> requiredAttributeDescriptors,
IEnumerable<string> allowedChildren,
string parentTag,
TagStructure tagStructure,
@ -266,7 +269,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
TypeName = typeName,
AssemblyName = assemblyName,
Attributes = attributeDescriptors,
RequiredAttributes = requiredAttributes,
RequiredAttributes = requiredAttributeDescriptors,
AllowedChildren = allowedChildren,
RequiredParent = parentTag,
TagStructure = tagStructure,
@ -274,15 +277,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
};
}
/// <summary>
/// Internal for testing.
/// </summary>
internal static IEnumerable<string> GetCommaSeparatedValues(string text)
{
// We don't want to remove empty entries, need to notify users of invalid values.
return text?.Split(',').Select(tagName => tagName.Trim()) ?? Enumerable.Empty<string>();
}
/// <summary>
/// Internal for testing.
/// </summary>
@ -291,20 +285,11 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
ErrorSink errorSink)
{
var validTagName = ValidateName(attribute.Tag, targetingAttributes: false, errorSink: errorSink);
var validAttributeNames = true;
var attributeNames = GetCommaSeparatedValues(attribute.Attributes);
foreach (var attributeName in attributeNames)
{
if (!ValidateName(attributeName, targetingAttributes: true, errorSink: errorSink))
{
validAttributeNames = false;
}
}
IEnumerable<TagHelperRequiredAttributeDescriptor> requiredAttributeDescriptors;
var validRequiredAttributes = TryGetRequiredAttributeDescriptors(attribute.Attributes, errorSink, out requiredAttributeDescriptors);
var validParentTagName = ValidateParentTagName(attribute.ParentTag, errorSink);
return validTagName && validAttributeNames && validParentTagName;
return validTagName && validRequiredAttributes && validParentTagName;
}
/// <summary>
@ -325,10 +310,17 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
errorSink: errorSink);
}
private static bool ValidateName(
string name,
bool targetingAttributes,
ErrorSink errorSink)
private static bool TryGetRequiredAttributeDescriptors(
string requiredAttributes,
ErrorSink errorSink,
out IEnumerable<TagHelperRequiredAttributeDescriptor> descriptors)
{
var parser = new RequiredAttributeParser(requiredAttributes);
return parser.TryParse(errorSink, out descriptors);
}
private static bool ValidateName(string name, bool targetingAttributes, ErrorSink errorSink)
{
if (!targetingAttributes &&
string.Equals(
@ -339,15 +331,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
// '*' as the entire name is OK in the HtmlTargetElement catch-all case.
return true;
}
else if (targetingAttributes &&
name.EndsWith(
TagHelperDescriptorProvider.RequiredAttributeWildcardSuffix,
StringComparison.OrdinalIgnoreCase))
{
// A single '*' at the end of a required attribute is valid; everywhere else is invalid. Strip it from
// the end so we can validate the rest of the name.
name = name.Substring(0, name.Length - 1);
}
var targetName = targetingAttributes ?
Resources.TagHelperDescriptorFactory_Attribute :
@ -750,5 +733,329 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
{
return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant();
}
// Internal for testing
internal class RequiredAttributeParser
{
private static readonly IReadOnlyDictionary<char, TagHelperRequiredAttributeValueComparison> CssValueComparisons =
new Dictionary<char, TagHelperRequiredAttributeValueComparison>
{
{ '=', TagHelperRequiredAttributeValueComparison.FullMatch },
{ '^', TagHelperRequiredAttributeValueComparison.PrefixMatch },
{ '$', TagHelperRequiredAttributeValueComparison.SuffixMatch }
};
private static readonly char[] InvalidPlainAttributeNameCharacters = { ' ', '\t', ',', RequiredAttributeWildcardSuffix };
private static readonly char[] InvalidCssAttributeNameCharacters = (new[] { ' ', '\t', ',', ']' })
.Concat(CssValueComparisons.Keys)
.ToArray();
private static readonly char[] InvalidCssQuotelessValueCharacters = { ' ', '\t', ']' };
private int _index;
private string _requiredAttributes;
public RequiredAttributeParser(string requiredAttributes)
{
_requiredAttributes = requiredAttributes;
}
private char Current => _requiredAttributes[_index];
private bool AtEnd => _index >= _requiredAttributes.Length;
public bool TryParse(
ErrorSink errorSink,
out IEnumerable<TagHelperRequiredAttributeDescriptor> requiredAttributeDescriptors)
{
if (string.IsNullOrEmpty(_requiredAttributes))
{
requiredAttributeDescriptors = Enumerable.Empty<TagHelperRequiredAttributeDescriptor>();
return true;
}
requiredAttributeDescriptors = null;
var descriptors = new List<TagHelperRequiredAttributeDescriptor>();
PassOptionalWhitespace();
do
{
TagHelperRequiredAttributeDescriptor descriptor;
if (At('['))
{
descriptor = ParseCssSelector(errorSink);
}
else
{
descriptor = ParsePlainSelector(errorSink);
}
if (descriptor == null)
{
// Failed to create the descriptor due to an invalid required attribute.
return false;
}
else
{
descriptors.Add(descriptor);
}
PassOptionalWhitespace();
if (At(','))
{
_index++;
if (!EnsureNotAtEnd(errorSink))
{
return false;
}
}
else if (!AtEnd)
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter(Current, _requiredAttributes),
length: 0);
return false;
}
PassOptionalWhitespace();
}
while (!AtEnd);
requiredAttributeDescriptors = descriptors;
return true;
}
private TagHelperRequiredAttributeDescriptor ParsePlainSelector(ErrorSink errorSink)
{
var nameEndIndex = _requiredAttributes.IndexOfAny(InvalidPlainAttributeNameCharacters, _index);
string attributeName;
var nameComparison = TagHelperRequiredAttributeNameComparison.FullMatch;
if (nameEndIndex == -1)
{
attributeName = _requiredAttributes.Substring(_index);
_index = _requiredAttributes.Length;
}
else
{
attributeName = _requiredAttributes.Substring(_index, nameEndIndex - _index);
_index = nameEndIndex;
if (_requiredAttributes[nameEndIndex] == RequiredAttributeWildcardSuffix)
{
nameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch;
// Move past wild card
_index++;
}
}
TagHelperRequiredAttributeDescriptor descriptor = null;
if (ValidateName(attributeName, targetingAttributes: true, errorSink: errorSink))
{
descriptor = new TagHelperRequiredAttributeDescriptor
{
Name = attributeName,
NameComparison = nameComparison
};
}
return descriptor;
}
private string ParseCssAttributeName(ErrorSink errorSink)
{
var nameStartIndex = _index;
var nameEndIndex = _requiredAttributes.IndexOfAny(InvalidCssAttributeNameCharacters, _index);
nameEndIndex = nameEndIndex == -1 ? _requiredAttributes.Length : nameEndIndex;
_index = nameEndIndex;
var attributeName = _requiredAttributes.Substring(nameStartIndex, nameEndIndex - nameStartIndex);
return attributeName;
}
private TagHelperRequiredAttributeValueComparison? ParseCssValueComparison(ErrorSink errorSink)
{
Debug.Assert(!AtEnd);
TagHelperRequiredAttributeValueComparison valueComparison;
if (CssValueComparisons.TryGetValue(Current, out valueComparison))
{
var op = Current;
_index++;
if (op != '=' && At('='))
{
// Two length operator (ex: ^=). Move past the second piece
_index++;
}
else if (op != '=') // We're at an incomplete operator (ex: [foo^]
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_PartialRequiredAttributeOperator(_requiredAttributes, op),
length: 0);
return null;
}
}
else if (!At(']'))
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeOperator(Current, _requiredAttributes),
length: 0);
return null;
}
return valueComparison;
}
private string ParseCssValue(ErrorSink errorSink)
{
int valueStart;
int valueEnd;
if (At('\'') || At('"'))
{
var quote = Current;
// Move past the quote
_index++;
valueStart = _index;
valueEnd = _requiredAttributes.IndexOf(quote, _index);
if (valueEnd == -1)
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes(
_requiredAttributes,
quote),
length: 0);
return null;
}
_index = valueEnd + 1;
}
else
{
valueStart = _index;
var valueEndIndex = _requiredAttributes.IndexOfAny(InvalidCssQuotelessValueCharacters, _index);
valueEnd = valueEndIndex == -1 ? _requiredAttributes.Length : valueEndIndex;
_index = valueEnd;
}
var value = _requiredAttributes.Substring(valueStart, valueEnd - valueStart);
return value;
}
private TagHelperRequiredAttributeDescriptor ParseCssSelector(ErrorSink errorSink)
{
Debug.Assert(At('['));
// Move past '['.
_index++;
PassOptionalWhitespace();
var attributeName = ParseCssAttributeName(errorSink);
PassOptionalWhitespace();
if (!EnsureNotAtEnd(errorSink))
{
return null;
}
if (!ValidateName(attributeName, targetingAttributes: true, errorSink: errorSink))
{
// Couldn't parse a valid attribute name.
return null;
}
var valueComparison = ParseCssValueComparison(errorSink);
if (!valueComparison.HasValue)
{
return null;
}
PassOptionalWhitespace();
if (!EnsureNotAtEnd(errorSink))
{
return null;
}
var value = ParseCssValue(errorSink);
if (value == null)
{
// Couldn't parse value
return null;
}
PassOptionalWhitespace();
if (At(']'))
{
// Move past the ending bracket.
_index++;
}
else if (AtEnd)
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace(_requiredAttributes),
length: 0);
return null;
}
else
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter(Current, _requiredAttributes),
length: 0);
return null;
}
return new TagHelperRequiredAttributeDescriptor
{
Name = attributeName,
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = value,
ValueComparison = valueComparison.Value,
};
}
private bool EnsureNotAtEnd(ErrorSink errorSink)
{
if (AtEnd)
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace(_requiredAttributes),
length: 0);
return false;
}
return true;
}
private bool At(char c)
{
return !AtEnd && Current == c;
}
private void PassOptionalWhitespace()
{
while (!AtEnd && (Current == ' ' || Current == '\t'))
{
_index++;
}
}
}
}
}

View File

@ -45,8 +45,10 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
public string Tag { get; }
/// <summary>
/// A comma-separated <see cref="string"/> of attribute names the HTML element must contain for the
/// <see cref="ITagHelper"/> to run. <c>*</c> at the end of an attribute name acts as a prefix match.
/// A comma-separated <see cref="string"/> of attribute selectors the HTML element must match for the
/// <see cref="ITagHelper"/> to run. <c>*</c> at the end of an attribute name acts as a prefix match. A value
/// surrounded by square brackets is handled as a CSS attribute value selector. Operators <c>^=</c>, <c>$=</c> and
/// <c>=</c> are supported e.g. <c>"name"</c>, <c>"[name]"</c>, <c>"[name=value]"</c>, <c>"[ name ^= 'value' ]"</c>.
/// </summary>
public string Attributes { get; set; }

View File

@ -33,7 +33,10 @@ namespace Microsoft.AspNetCore.Razor.Test.Internal
// 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)
@ -66,9 +69,10 @@ namespace Microsoft.AspNetCore.Razor.Test.Internal
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)

View File

@ -0,0 +1,43 @@
// 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;
}
}
}

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
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
@ -140,7 +140,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
/// <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
{

View File

@ -51,9 +51,9 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
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 &&
@ -80,11 +80,11 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
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)

View File

@ -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
{
@ -14,8 +15,6 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
{
public const string ElementCatchAllTarget = "*";
public static readonly string RequiredAttributeWildcardSuffix = "*";
private IDictionary<string, HashSet<TagHelperDescriptor>> _registrations;
private string _tagHelperPrefix;
@ -39,14 +38,14 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
/// </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) &&
@ -78,10 +77,10 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
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(
@ -95,37 +94,12 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
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.IsMatch(attribute.Key, attribute.Value))));
}
private void Register(TagHelperDescriptor descriptor)

View File

@ -0,0 +1,82 @@
// 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.Diagnostics;
using System.Linq;
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 comparison method to use for <see cref="Name"/> when determining if an HTML attribute name matches.
/// </summary>
public TagHelperRequiredAttributeNameComparison NameComparison { get; set; }
/// <summary>
/// The HTML attribute value.
/// </summary>
public string Value { get; set; }
/// <summary>
/// The comparison method to use for <see cref="Value"/> when determining if an HTML attribute value matches.
/// </summary>
public TagHelperRequiredAttributeValueComparison ValueComparison { 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><c>true</c> if the current <see cref="TagHelperRequiredAttributeDescriptor"/> matches
/// <paramref name="attributeName"/> and <paramref name="attributeValue"/>; <c>false</c> otherwise.</returns>
public bool IsMatch(string attributeName, string attributeValue)
{
var nameMatches = false;
if (NameComparison == TagHelperRequiredAttributeNameComparison.FullMatch)
{
nameMatches = string.Equals(Name, attributeName, StringComparison.OrdinalIgnoreCase);
}
else if (NameComparison == TagHelperRequiredAttributeNameComparison.PrefixMatch)
{
// attributeName cannot equal the Name if comparing as a PrefixMatch.
nameMatches = attributeName.Length != Name.Length &&
attributeName.StartsWith(Name, StringComparison.OrdinalIgnoreCase);
}
else
{
Debug.Assert(false, "Unknown name comparison.");
}
if (!nameMatches)
{
return false;
}
switch (ValueComparison)
{
case TagHelperRequiredAttributeValueComparison.None:
return true;
case TagHelperRequiredAttributeValueComparison.PrefixMatch: // Value starts with
return attributeValue.StartsWith(Value, StringComparison.Ordinal);
case TagHelperRequiredAttributeValueComparison.SuffixMatch: // Value ends with
return attributeValue.EndsWith(Value, StringComparison.Ordinal);
case TagHelperRequiredAttributeValueComparison.FullMatch: // Value equals
return string.Equals(attributeValue, Value, StringComparison.Ordinal);
default:
Debug.Assert(false, "Unknown value comparison.");
return false;
}
}
}
}

View File

@ -0,0 +1,58 @@
// 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.NameComparison == descriptorY.NameComparison &&
descriptorX.ValueComparison == descriptorY.ValueComparison &&
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.NameComparison);
hashCodeCombiner.Add(descriptor.ValueComparison);
hashCodeCombiner.Add(descriptor.Name, StringComparer.OrdinalIgnoreCase);
hashCodeCombiner.Add(descriptor.Value, StringComparer.Ordinal);
return hashCodeCombiner.CombinedHash;
}
}
}

View File

@ -0,0 +1,21 @@
// 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.
namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
{
/// <summary>
/// Acceptable <see cref="TagHelperRequiredAttributeDescriptor.Name"/> comparison modes.
/// </summary>
public enum TagHelperRequiredAttributeNameComparison
{
/// <summary>
/// HTML attribute name case insensitively matches <see cref="TagHelperRequiredAttributeDescriptor.Name"/>.
/// </summary>
FullMatch,
/// <summary>
/// HTML attribute name case insensitively starts with <see cref="TagHelperRequiredAttributeDescriptor.Name"/>.
/// </summary>
PrefixMatch,
}
}

View File

@ -0,0 +1,31 @@
// 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.
namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
{
/// <summary>
/// Acceptable <see cref="TagHelperRequiredAttributeDescriptor.Value"/> comparison modes.
/// </summary>
public enum TagHelperRequiredAttributeValueComparison
{
/// <summary>
/// HTML attribute value always matches <see cref="TagHelperRequiredAttributeDescriptor.Value"/>.
/// </summary>
None,
/// <summary>
/// HTML attribute value case sensitively matches <see cref="TagHelperRequiredAttributeDescriptor.Value"/>.
/// </summary>
FullMatch,
/// <summary>
/// HTML attribute value case sensitively starts with <see cref="TagHelperRequiredAttributeDescriptor.Value"/>.
/// </summary>
PrefixMatch,
/// <summary>
/// HTML attribute value case sensitively ends with <see cref="TagHelperRequiredAttributeDescriptor.Value"/>.
/// </summary>
SuffixMatch,
}
}

View File

@ -239,7 +239,8 @@ namespace Microsoft.AspNetCore.Razor.Parser
return addOrRemoveTagHelperSpanVisitor.GetDescriptors(documentRoot);
}
private static IEnumerable<ISyntaxTreeRewriter> GetDefaultRewriters(ParserBase markupParser)
// Internal for testing
internal static IEnumerable<ISyntaxTreeRewriter> GetDefaultRewriters(ParserBase markupParser)
{
return new ISyntaxTreeRewriter[]
{

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
using Microsoft.AspNetCore.Razor.TagHelpers;
@ -14,6 +15,10 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
{
public class TagHelperParseTreeRewriter : ISyntaxTreeRewriter
{
// Internal for testing.
// Null characters are invalid markup for HTML attribute values.
internal static readonly string InvalidAttributeValueMarker = "\0";
// From http://dev.w3.org/html5/spec/Overview.html#elements-0
private static readonly HashSet<string> VoidElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
@ -35,10 +40,12 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
"wbr"
};
private TagHelperDescriptorProvider _provider;
private Stack<TagBlockTracker> _trackerStack;
private readonly List<KeyValuePair<string, string>> _htmlAttributeTracker;
private readonly StringBuilder _attributeValueBuilder;
private readonly TagHelperDescriptorProvider _provider;
private readonly Stack<TagBlockTracker> _trackerStack;
private readonly Stack<BlockBuilder> _blockStack;
private TagHelperBlockTracker _currentTagHelperTracker;
private Stack<BlockBuilder> _blockStack;
private BlockBuilder _currentBlock;
private string _currentParentTagName;
@ -47,6 +54,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
_provider = provider;
_trackerStack = new Stack<TagBlockTracker>();
_blockStack = new Stack<BlockBuilder>();
_attributeValueBuilder = new StringBuilder();
_htmlAttributeTracker = new List<KeyValuePair<string, string>>();
}
public void Rewrite(RewritingContext context)
@ -177,7 +186,7 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
if (!IsEndTag(tagBlock))
{
// We're now in a start tag block, we first need to see if the tag block is a tag helper.
var providedAttributes = GetAttributeNames(tagBlock);
var providedAttributes = GetAttributeNameValuePairs(tagBlock);
descriptors = _provider.GetDescriptors(tagName, providedAttributes, _currentParentTagName);
@ -246,7 +255,7 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
{
descriptors = _provider.GetDescriptors(
tagName,
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: _currentParentTagName);
// If there are not TagHelperDescriptors associated with the end tag block that also have no
@ -299,7 +308,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
return true;
}
private IEnumerable<string> GetAttributeNames(Block tagBlock)
// Internal for testing
internal IEnumerable<KeyValuePair<string, string>> GetAttributeNameValuePairs(Block tagBlock)
{
// Need to calculate how many children we should take that represent the attributes.
var childrenOffset = IsPartialTag(tagBlock) ? 0 : 1;
@ -307,32 +317,112 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
if (childCount <= 1)
{
return Enumerable.Empty<string>();
return Enumerable.Empty<KeyValuePair<string, string>>();
}
var attributeChildren = new List<SyntaxTreeNode>(childCount - 1);
_htmlAttributeTracker.Clear();
var attributes = _htmlAttributeTracker;
for (var i = 1; i < childCount; i++)
{
attributeChildren.Add(tagBlock.Children[i]);
}
var attributeNames = new List<string>();
foreach (var child in attributeChildren)
{
var child = tagBlock.Children[i];
Span childSpan;
if (child.IsBlock)
{
childSpan = ((Block)child).FindFirstDescendentSpan();
var childBlock = (Block)child;
if (childBlock.Type != BlockType.Markup)
{
// Anything other than markup blocks in the attribute area of tags mangles following attributes.
// It's also not supported by TagHelpers, bail early to avoid creating bad attribute value pairs.
break;
}
childSpan = childBlock.FindFirstDescendentSpan();
if (childSpan == null)
{
_attributeValueBuilder.Append(InvalidAttributeValueMarker);
continue;
}
// We can assume the first span will always contain attributename=" and the last span will always
// contain the final quote. Therefore, if the values not quoted there's no ending quote to skip.
var childOffset = 0;
if (childSpan.Symbols.Count > 0)
{
var potentialQuote = childSpan.Symbols[childSpan.Symbols.Count - 1] as HtmlSymbol;
if (potentialQuote != null &&
(potentialQuote.Type == HtmlSymbolType.DoubleQuote ||
potentialQuote.Type == HtmlSymbolType.SingleQuote))
{
childOffset = 1;
}
}
for (var j = 1; j < childBlock.Children.Count - childOffset; j++)
{
var valueChild = childBlock.Children[j];
if (valueChild.IsBlock)
{
_attributeValueBuilder.Append(InvalidAttributeValueMarker);
}
else
{
var valueChildSpan = (Span)valueChild;
for (var k = 0; k < valueChildSpan.Symbols.Count; k++)
{
_attributeValueBuilder.Append(valueChildSpan.Symbols[k].Content);
}
}
}
}
else
{
childSpan = child as Span;
childSpan = (Span)child;
var afterEquals = false;
var atValue = false;
var endValueMarker = childSpan.Symbols.Count;
// Entire attribute is a string
for (var j = 0; j < endValueMarker; j++)
{
var htmlSymbol = (HtmlSymbol)childSpan.Symbols[j];
if (!afterEquals)
{
afterEquals = htmlSymbol.Type == HtmlSymbolType.Equals;
continue;
}
if (!atValue)
{
atValue = htmlSymbol.Type != HtmlSymbolType.WhiteSpace &&
htmlSymbol.Type != HtmlSymbolType.NewLine;
if (atValue)
{
if (htmlSymbol.Type == HtmlSymbolType.DoubleQuote ||
htmlSymbol.Type == HtmlSymbolType.SingleQuote)
{
endValueMarker--;
}
else
{
// Current symbol is considered the value (unquoted). Add its content to the
// attribute value builder before we move past it.
_attributeValueBuilder.Append(htmlSymbol.Content);
}
}
continue;
}
_attributeValueBuilder.Append(htmlSymbol.Content);
}
}
var start = 0;
@ -344,8 +434,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
}
}
var end = 0;
for (end = start; end < childSpan.Content.Length; end++)
var end = start;
for (; end < childSpan.Content.Length; end++)
{
if (childSpan.Content[end] == '=')
{
@ -353,10 +443,15 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
}
}
attributeNames.Add(childSpan.Content.Substring(start, end - start));
var attributeName = childSpan.Content.Substring(start, end - start);
var attributeValue = _attributeValueBuilder.ToString();
var attribute = new KeyValuePair<string, string>(attributeName, attributeValue);
attributes.Add(attribute);
_attributeValueBuilder.Clear();
}
return attributeNames;
return attributes;
}
private bool HasAllowedChildren()
@ -650,7 +745,7 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
{
var child = tagBlock.Children[0];
if (tagBlock.Type != BlockType.Tag || tagBlock.Children.Count == 0|| !(child is Span))
if (tagBlock.Type != BlockType.Tag || tagBlock.Children.Count == 0 || !(child is Span))
{
return null;
}

View File

@ -19,6 +19,155 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
protected static readonly string AssemblyName = TagHelperDescriptorFactoryTestAssembly.Name;
public static TheoryData RequiredAttributeParserErrorData
{
get
{
Func<string, RazorError> error = (message) => new RazorError(message, SourceLocation.Zero, 0);
return new TheoryData<string, RazorError>
{
{ "name,", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("name,")) },
{ " ", error(Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace("Attribute")) },
{ "n@me", error(Resources.FormatHtmlTargetElementAttribute_InvalidName("attribute", "n@me", '@')) },
{ "name extra", error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter('e', "name extra")) },
{ "[[ ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[[ ")) },
{ "[ ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[ ")) },
{
"[name='unended]",
error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes("[name='unended]", '\''))
},
{
"[name='unended",
error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes("[name='unended", '\''))
},
{ "[name", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name")) },
{ "[ ]", error(Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace("Attribute")) },
{ "[n@me]", error(Resources.FormatHtmlTargetElementAttribute_InvalidName("attribute", "n@me", '@')) },
{ "[name@]", error(Resources.FormatHtmlTargetElementAttribute_InvalidName("attribute", "name@", '@')) },
{ "[name^]", error(Resources.FormatTagHelperDescriptorFactory_PartialRequiredAttributeOperator("[name^]", '^')) },
{ "[name='value'", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name='value'")) },
{ "[name ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name ")) },
{ "[name extra]", error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeOperator('e', "[name extra]")) },
{ "[name=value ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name=value ")) },
};
}
}
[Theory]
[MemberData(nameof(RequiredAttributeParserErrorData))]
public void RequiredAttributeParser_ParsesRequiredAttributesAndLogsErrorCorrectly(
string requiredAttributes,
RazorError expectedError)
{
// Arrange
var parser = new TagHelperDescriptorFactory.RequiredAttributeParser(requiredAttributes);
var errorSink = new ErrorSink();
IEnumerable<TagHelperRequiredAttributeDescriptor> descriptors;
// Act
var parsedCorrectly = parser.TryParse(errorSink, out descriptors);
// Assert
Assert.False(parsedCorrectly);
Assert.Null(descriptors);
var error = Assert.Single(errorSink.Errors);
Assert.Equal(expectedError, error);
}
public static TheoryData RequiredAttributeParserData
{
get
{
Func<string, TagHelperRequiredAttributeNameComparison, TagHelperRequiredAttributeDescriptor> plain =
(name, nameComparison) => new TagHelperRequiredAttributeDescriptor
{
Name = name,
NameComparison = nameComparison
};
Func<string, string, TagHelperRequiredAttributeValueComparison, TagHelperRequiredAttributeDescriptor> css =
(name, value, valueComparison) => new TagHelperRequiredAttributeDescriptor
{
Name = name,
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = value,
ValueComparison = valueComparison,
};
return new TheoryData<string, IEnumerable<TagHelperRequiredAttributeDescriptor>>
{
{ null, Enumerable.Empty<TagHelperRequiredAttributeDescriptor>() },
{ string.Empty, Enumerable.Empty<TagHelperRequiredAttributeDescriptor>() },
{ "name", new[] { plain("name", TagHelperRequiredAttributeNameComparison.FullMatch) } },
{ "name-*", new[] { plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch) } },
{ " name-* ", new[] { plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch) } },
{
"asp-route-*,valid , name-* ,extra",
new[]
{
plain("asp-route-", TagHelperRequiredAttributeNameComparison.PrefixMatch),
plain("valid", TagHelperRequiredAttributeNameComparison.FullMatch),
plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch),
plain("extra", TagHelperRequiredAttributeNameComparison.FullMatch),
}
},
{ "[name]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.None) } },
{ "[ name ]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.None) } },
{ " [ name ] ", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.None) } },
{ "[name=]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.FullMatch) } },
{ "[name='']", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.FullMatch) } },
{ "[name ^=]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.PrefixMatch) } },
{ "[name=hello]", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } },
{ "[name= hello]", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } },
{ "[name='hello']", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } },
{ "[name=\"hello\"]", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } },
{ " [ name $= \" hello\" ] ", new[] { css("name", " hello", TagHelperRequiredAttributeValueComparison.SuffixMatch) } },
{
"[name=\"hello\"],[other^=something ], [val = 'cool']",
new[]
{
css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch),
css("other", "something", TagHelperRequiredAttributeValueComparison.PrefixMatch),
css("val", "cool", TagHelperRequiredAttributeValueComparison.FullMatch) }
},
{
"asp-route-*,[name=\"hello\"],valid ,[other^=something ], name-* ,[val = 'cool'],extra",
new[]
{
plain("asp-route-", TagHelperRequiredAttributeNameComparison.PrefixMatch),
css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch),
plain("valid", TagHelperRequiredAttributeNameComparison.FullMatch),
css("other", "something", TagHelperRequiredAttributeValueComparison.PrefixMatch),
plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch),
css("val", "cool", TagHelperRequiredAttributeValueComparison.FullMatch),
plain("extra", TagHelperRequiredAttributeNameComparison.FullMatch),
}
},
};
}
}
[Theory]
[MemberData(nameof(RequiredAttributeParserData))]
public void RequiredAttributeParser_ParsesRequiredAttributesCorrectly(
string requiredAttributes,
IEnumerable<TagHelperRequiredAttributeDescriptor> expectedDescriptors)
{
// Arrange
var parser = new TagHelperDescriptorFactory.RequiredAttributeParser(requiredAttributes);
var errorSink = new ErrorSink();
IEnumerable<TagHelperRequiredAttributeDescriptor> descriptors;
// Act
//System.Diagnostics.Debugger.Launch();
var parsedCorrectly = parser.TryParse(errorSink, out descriptors);
// Assert
Assert.True(parsedCorrectly);
Assert.Empty(errorSink.Errors);
Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.Default);
}
public static TheoryData IsEnumData
{
get
@ -617,7 +766,10 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(AttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class" })
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" }
})
}
},
{
@ -629,7 +781,11 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class", "style" })
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" },
new TagHelperRequiredAttributeDescriptor { Name = "style" }
})
}
},
{
@ -641,13 +797,20 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiAttributeAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "custom" }),
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "custom" }
}),
CreateTagHelperDescriptor(
TagHelperDescriptorProvider.ElementCatchAllTarget,
typeof(MultiAttributeAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class", "style" })
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" },
new TagHelperRequiredAttributeDescriptor { Name = "style" }
})
}
},
{
@ -659,7 +822,10 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(InheritedAttributeTargetingTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "style" })
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "style" }
})
}
},
{
@ -671,7 +837,10 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(RequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class" })
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" }
})
}
},
{
@ -683,7 +852,10 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(InheritedRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class" })
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" }
})
}
},
{
@ -695,13 +867,19 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiAttributeRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class" }),
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" }
}),
CreateTagHelperDescriptor(
"input",
typeof(MultiAttributeRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class" })
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" }
})
}
},
{
@ -713,13 +891,19 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiAttributeSameTagRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "style" }),
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "style" }
}),
CreateTagHelperDescriptor(
"input",
typeof(MultiAttributeSameTagRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class" })
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" }
})
}
},
{
@ -731,7 +915,11 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class", "style" })
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" },
new TagHelperRequiredAttributeDescriptor { Name = "style" }
})
}
},
{
@ -743,13 +931,20 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiTagMultiRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class", "style" }),
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" },
new TagHelperRequiredAttributeDescriptor { Name = "style" }
}),
CreateTagHelperDescriptor(
"input",
typeof(MultiTagMultiRequiredAttributeTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class", "style" }),
requiredAttributes: new[] {
new TagHelperRequiredAttributeDescriptor { Name = "class" },
new TagHelperRequiredAttributeDescriptor { Name = "style" }
}),
}
},
{
@ -761,7 +956,14 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(AttributeWildcardTargetingTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class*" })
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "class",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
}
})
}
},
{
@ -773,7 +975,19 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
typeof(MultiAttributeWildcardTargetingTagHelper).FullName,
AssemblyName,
attributes,
requiredAttributes: new[] { "class*", "style*" })
requiredAttributes: new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "class",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
},
new TagHelperRequiredAttributeDescriptor
{
Name = "style",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
}
})
}
},
};
@ -1327,29 +1541,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
}
}
[Theory]
[MemberData(nameof(ValidNameData))]
public void GetCommaSeparatedValues_OutputsCommaSeparatedListOfNames(
string name,
IEnumerable<string> expectedNames)
{
// Act
var result = TagHelperDescriptorFactory.GetCommaSeparatedValues(name);
// Assert
Assert.Equal(expectedNames, result);
}
[Fact]
public void GetCommaSeparatedValues_OutputsEmptyArrayForNullValue()
{
// Act
var result = TagHelperDescriptorFactory.GetCommaSeparatedValues(text: null);
// Assert
Assert.Empty(result);
}
public static TheoryData InvalidTagHelperAttributeDescriptorData
{
get
@ -2293,7 +2484,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
string typeName,
string assemblyName,
IEnumerable<TagHelperAttributeDescriptor> attributes = null,
IEnumerable<string> requiredAttributes = null)
IEnumerable<TagHelperRequiredAttributeDescriptor> requiredAttributes = null)
{
return new TagHelperDescriptor
{
@ -2301,7 +2492,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
TypeName = typeName,
AssemblyName = assemblyName,
Attributes = attributes ?? Enumerable.Empty<TagHelperAttributeDescriptor>(),
RequiredAttributes = requiredAttributes ?? Enumerable.Empty<string>()
RequiredAttributes = requiredAttributes ?? Enumerable.Empty<TagHelperRequiredAttributeDescriptor>()
};
}

View File

@ -21,6 +21,126 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
private static IEnumerable<TagHelperDescriptor> PrefixedPAndInputTagHelperDescriptors { get; }
= BuildPAndInputTagHelperDescriptors(prefix: "THS");
private static IEnumerable<TagHelperDescriptor> CssSelectorTagHelperDescriptors
{
get
{
var inputTypePropertyInfo = typeof(TestType).GetProperty("Type");
var inputCheckedPropertyInfo = typeof(TestType).GetProperty("Checked");
return new[]
{
new TagHelperDescriptor
{
TagName = "a",
TypeName = "TestNamespace.ATagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "href",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "~/",
ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch,
}
},
},
new TagHelperDescriptor
{
TagName = "a",
TypeName = "TestNamespace.ATagHelperMultipleSelectors",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "href",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "~/",
ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
},
new TagHelperRequiredAttributeDescriptor
{
Name = "href",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "?hello=world",
ValueComparison = TagHelperRequiredAttributeValueComparison.SuffixMatch,
}
},
},
new TagHelperDescriptor
{
TagName = "input",
TypeName = "TestNamespace.InputTagHelper",
AssemblyName = "SomeAssembly",
Attributes = new TagHelperAttributeDescriptor[]
{
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
},
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "type",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "text",
ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch,
}
},
},
new TagHelperDescriptor
{
TagName = "input",
TypeName = "TestNamespace.InputTagHelper2",
AssemblyName = "SomeAssembly",
Attributes = new TagHelperAttributeDescriptor[]
{
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
},
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "ty",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
}
},
},
new TagHelperDescriptor
{
TagName = "*",
TypeName = "TestNamespace.CatchAllTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "href",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "~/",
ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
}
},
},
new TagHelperDescriptor
{
TagName = "*",
TypeName = "TestNamespace.CatchAllTagHelper2",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "type",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
}
},
}
};
}
}
private static IEnumerable<TagHelperDescriptor> EnumTagHelperDescriptors
{
get
@ -113,7 +233,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
TypeName = typeof(string).FullName
},
},
RequiredAttributes = new[] { "bound" },
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "bound" } },
},
};
}
@ -140,7 +260,10 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
IsStringProperty = true
}
},
RequiredAttributes = new[] { "catchall-unbound-required" },
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "catchall-unbound-required" }
},
},
new TagHelperDescriptor
{
@ -164,7 +287,11 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
IsStringProperty = true
}
},
RequiredAttributes = new[] { "input-bound-required-string", "input-unbound-required" },
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "input-bound-required-string" },
new TagHelperRequiredAttributeDescriptor { Name = "input-unbound-required" }
},
}
};
}
@ -214,7 +341,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
RequiredAttributes = new[] { "type" },
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "type" } },
},
new TagHelperDescriptor
{
@ -226,7 +353,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
RequiredAttributes = new[] { "checked" },
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "checked" } },
},
new TagHelperDescriptor
{
@ -238,7 +365,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
RequiredAttributes = new[] { "type" },
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "type" } },
},
new TagHelperDescriptor
{
@ -250,7 +377,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
RequiredAttributes = new[] { "checked" },
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "checked" } },
}
};
}
@ -269,7 +396,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
TagName = "p",
TypeName = "TestNamespace.PTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "class" },
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } },
},
new TagHelperDescriptor
{
@ -280,7 +407,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
{
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo)
},
RequiredAttributes = new[] { "type" },
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "type" } },
},
new TagHelperDescriptor
{
@ -292,14 +419,18 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
RequiredAttributes = new[] { "type", "checked" },
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "type" },
new TagHelperRequiredAttributeDescriptor { Name = "checked" }
},
},
new TagHelperDescriptor
{
TagName = "*",
TypeName = "TestNamespace.CatchAllTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "catchAll" },
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "catchAll" } },
}
};
}
@ -1774,6 +1905,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Generator
// Note: The baseline resource name is equivalent to the test resource name.
return new TheoryData<string, string, IEnumerable<TagHelperDescriptor>>
{
{ "CssSelectorTagHelperAttributes", null, CssSelectorTagHelperDescriptors },
{ "IncompleteTagHelper", null, DefaultPAndInputTagHelperDescriptors },
{ "SingleTagHelper", null, DefaultPAndInputTagHelperDescriptors },
{ "SingleTagHelperWithNewlineBeforeAttributes", null, DefaultPAndInputTagHelperDescriptors },

View File

@ -157,7 +157,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
TypeName = typeof(string).FullName
},
},
RequiredAttributes = new[] { "bound" },
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "bound" } },
},
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
@ -3940,7 +3940,10 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
IsStringProperty = true
}
},
RequiredAttributes = new[] { "unbound-required" }
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "unbound-required" }
}
},
new TagHelperDescriptor
{
@ -3957,7 +3960,10 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
IsStringProperty = true
}
},
RequiredAttributes = new[] { "bound-required-string" }
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "bound-required-string" }
}
},
new TagHelperDescriptor
{
@ -3973,7 +3979,10 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
TypeName = typeof(int).FullName
}
},
RequiredAttributes = new[] { "bound-required-int" }
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "bound-required-int" }
}
},
new TagHelperDescriptor
{

View File

@ -1,9 +1,9 @@
// 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.Linq;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.Test.Internal;
using Xunit;
@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
[Theory]
[MemberData(nameof(RequiredParentData))]
public void GetDescriptors_ReturnsDescriptorsWithRequiredAttributes(
public void GetDescriptors_ReturnsDescriptorsParentTags(
string tagName,
string parentTagName,
IEnumerable<TagHelperDescriptor> availableDescriptors,
@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var resolvedDescriptors = provider.GetDescriptors(
tagName,
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: parentTagName);
// Assert
@ -101,131 +101,155 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
TagName = "div",
TypeName = "DivTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "style" }
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "style" } }
};
var inputDescriptor = new TagHelperDescriptor
{
TagName = "input",
TypeName = "InputTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "class", "style" }
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" },
new TagHelperRequiredAttributeDescriptor { Name = "style" }
}
};
var inputWildcardPrefixDescriptor = new TagHelperDescriptor
{
TagName = "input",
TypeName = "InputWildCardAttribute",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "nodashprefix*" }
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "nodashprefix",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
}
}
};
var catchAllDescriptor = new TagHelperDescriptor
{
TagName = TagHelperDescriptorProvider.ElementCatchAllTarget,
TypeName = "CatchAllTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "class" }
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } }
};
var catchAllDescriptor2 = new TagHelperDescriptor
{
TagName = TagHelperDescriptorProvider.ElementCatchAllTarget,
TypeName = "CatchAllTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "custom", "class" }
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "custom" },
new TagHelperRequiredAttributeDescriptor { Name = "class" }
}
};
var catchAllWildcardPrefixDescriptor = new TagHelperDescriptor
{
TagName = TagHelperDescriptorProvider.ElementCatchAllTarget,
TypeName = "CatchAllWildCardAttribute",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "prefix-*" }
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "prefix-",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
}
}
};
var defaultAvailableDescriptors =
new[] { divDescriptor, inputDescriptor, catchAllDescriptor, catchAllDescriptor2 };
var defaultWildcardDescriptors =
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor };
Func<string, KeyValuePair<string, string>> kvp =
(name) => new KeyValuePair<string, string>(name, "test value");
return new TheoryData<
string, // tagName
IEnumerable<string>, // providedAttributes
IEnumerable<KeyValuePair<string, string>>, // providedAttributes
IEnumerable<TagHelperDescriptor>, // availableDescriptors
IEnumerable<TagHelperDescriptor>> // expectedDescriptors
{
{
"div",
new[] { "custom" },
new[] { kvp("custom") },
defaultAvailableDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{ "div", new[] { "style" }, defaultAvailableDescriptors, new[] { divDescriptor } },
{ "div", new[] { "class" }, defaultAvailableDescriptors, new[] { catchAllDescriptor } },
{ "div", new[] { kvp("style") }, defaultAvailableDescriptors, new[] { divDescriptor } },
{ "div", new[] { kvp("class") }, defaultAvailableDescriptors, new[] { catchAllDescriptor } },
{
"div",
new[] { "class", "style" },
new[] { kvp("class"), kvp("style") },
defaultAvailableDescriptors,
new[] { divDescriptor, catchAllDescriptor }
},
{
"div",
new[] { "class", "style", "custom" },
new[] { kvp("class"), kvp("style"), kvp("custom") },
defaultAvailableDescriptors,
new[] { divDescriptor, catchAllDescriptor, catchAllDescriptor2 }
},
{
"input",
new[] { "class", "style" },
new[] { kvp("class"), kvp("style") },
defaultAvailableDescriptors,
new[] { inputDescriptor, catchAllDescriptor }
},
{
"input",
new[] { "nodashprefixA" },
new[] { kvp("nodashprefixA") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor }
},
{
"input",
new[] { "nodashprefix-ABC-DEF", "random" },
new[] { kvp("nodashprefix-ABC-DEF"), kvp("random") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor }
},
{
"input",
new[] { "prefixABCnodashprefix" },
new[] { kvp("prefixABCnodashprefix") },
defaultWildcardDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{
"input",
new[] { "prefix-" },
new[] { kvp("prefix-") },
defaultWildcardDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{
"input",
new[] { "nodashprefix" },
new[] { kvp("nodashprefix") },
defaultWildcardDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{
"input",
new[] { "prefix-A" },
new[] { kvp("prefix-A") },
defaultWildcardDescriptors,
new[] { catchAllWildcardPrefixDescriptor }
},
{
"input",
new[] { "prefix-ABC-DEF", "random" },
new[] { kvp("prefix-ABC-DEF"), kvp("random") },
defaultWildcardDescriptors,
new[] { catchAllWildcardPrefixDescriptor }
},
{
"input",
new[] { "prefix-abc", "nodashprefix-def" },
new[] { kvp("prefix-abc"), kvp("nodashprefix-def") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor }
},
{
"input",
new[] { "class", "prefix-abc", "onclick", "nodashprefix-def", "style" },
new[] { kvp("class"), kvp("prefix-abc"), kvp("onclick"), kvp("nodashprefix-def"), kvp("style") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor }
},
@ -237,7 +261,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
[MemberData(nameof(RequiredAttributeData))]
public void GetDescriptors_ReturnsDescriptorsWithRequiredAttributes(
string tagName,
IEnumerable<string> providedAttributes,
IEnumerable<KeyValuePair<string, string>> providedAttributes,
IEnumerable<TagHelperDescriptor> availableDescriptors,
IEnumerable<TagHelperDescriptor> expectedDescriptors)
{
@ -265,7 +289,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var resolvedDescriptors = provider.GetDescriptors(
tagName: "th",
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
@ -284,11 +308,11 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "th:div",
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
var retrievedDescriptorsSpan = provider.GetDescriptors(
tagName: "th2:span",
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
@ -308,11 +332,11 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "th:div",
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
var retrievedDescriptorsSpan = provider.GetDescriptors(
tagName: "th:span",
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
@ -333,7 +357,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptors = provider.GetDescriptors(
tagName: "th:div",
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
@ -354,7 +378,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "div",
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
@ -383,7 +407,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptors = provider.GetDescriptors(
tagName: "foo",
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
@ -418,11 +442,11 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var divDescriptors = provider.GetDescriptors(
tagName: "div",
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
var spanDescriptors = provider.GetDescriptors(
tagName: "span",
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
@ -453,7 +477,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
// Act
var retrievedDescriptors = provider.GetDescriptors(
tagName: "div",
attributeNames: Enumerable.Empty<string>(),
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert

View File

@ -21,8 +21,22 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
Prefix = "prefix:",
TagName = "tag name",
TypeName = "type name",
AssemblyName = "assembly name",
RequiredAttributes = new[] { "required attribute one", "required attribute two" },
AssemblyName = "assembly name",
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "required attribute one",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch
},
new TagHelperRequiredAttributeDescriptor
{
Name = "required attribute two",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "something",
ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
}
},
AllowedChildren = new[] { "allowed child one" },
RequiredParent = "parent name",
DesignTimeDescriptor = new TagHelperDesignTimeDescriptor
@ -41,7 +55,14 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" +
"[\"required attribute one\",\"required attribute two\"]," +
$"[{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute one\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":1," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":null," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":0}}," +
$"{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute two\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":0," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":\"something\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":2}}]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\"]," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," +
@ -200,8 +221,15 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
$"\"{nameof(TagHelperDescriptor.TypeName)}\":\"type name\"," +
$"\"{nameof(TagHelperDescriptor.AssemblyName)}\":\"assembly name\"," +
$"\"{nameof(TagHelperDescriptor.Attributes)}\":[]," +
$"\"{nameof(TagHelperDescriptor.RequiredAttributes)}\":" +
"[\"required attribute one\",\"required attribute two\"]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" +
$"[{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute one\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":1," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":null," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":0}}," +
$"{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute two\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":0," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":\"something\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":2}}]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," +
$"\"{nameof(TagHelperDescriptor.TagStructure)}\":2," +
@ -215,7 +243,21 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
TagName = "tag name",
TypeName = "type name",
AssemblyName = "assembly name",
RequiredAttributes = new[] { "required attribute one", "required attribute two" },
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "required attribute one",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch
},
new TagHelperRequiredAttributeDescriptor
{
Name = "required attribute two",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "something",
ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
}
},
AllowedChildren = new[] { "allowed child one", "allowed child two" },
RequiredParent = "parent name",
DesignTimeDescriptor = new TagHelperDesignTimeDescriptor
@ -237,7 +279,7 @@ namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
Assert.Empty(descriptor.Attributes);
Assert.Equal(expectedDescriptor.RequiredAttributes, descriptor.RequiredAttributes, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.RequiredAttributes, descriptor.RequiredAttributes, TagHelperRequiredAttributeDescriptorComparer.Default);
Assert.Equal(
expectedDescriptor.DesignTimeDescriptor,
descriptor.DesignTimeDescriptor,

View File

@ -13,11 +13,74 @@ using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
using Microsoft.AspNetCore.Razor.Test.Framework;
using Microsoft.AspNetCore.Razor.Text;
using Xunit;
using Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal;
namespace Microsoft.AspNetCore.Razor.Test.TagHelpers
{
public class TagHelperParseTreeRewriterTest : TagHelperRewritingTestBase
{
public static TheoryData GetAttributeNameValuePairsData
{
get
{
var factory = CreateDefaultSpanFactory();
var blockFactory = new BlockFactory(factory);
Func<string, string, KeyValuePair<string, string>> kvp =
(key, value) => new KeyValuePair<string, string>(key, value);
var empty = Enumerable.Empty<KeyValuePair<string, string>>();
var csharp = TagHelperParseTreeRewriter.InvalidAttributeValueMarker;
// documentContent, expectedPairs
return new TheoryData<string, IEnumerable<KeyValuePair<string, string>>>
{
{ "<a>", empty },
{ "<a @{ } href='~/home'>", empty },
{ "<a href=\"@true\">", new[] { kvp("href", csharp) } },
{ "<a href=\"prefix @true suffix\">", new[] { kvp("href", $"prefix{csharp} suffix") } },
{ "<a href=~/home>", new[] { kvp("href", "~/home") } },
{ "<a href=~/home @{ } nothing='something'>", new[] { kvp("href", "~/home"), kvp("", "") } },
{
"<a href=\"@DateTime.Now::0\" class='btn btn-success' random>",
new[] { kvp("href", $"{csharp}::0"), kvp("class", "btn btn-success"), kvp("random", "") }
},
{ "<a href=>", new[] { kvp("href", "") } },
{ "<a href='\"> ", new[] { kvp("href", "\">") } },
{ "<a href'", new[] { kvp("href'", "") } },
};
}
}
[Theory]
[MemberData(nameof(GetAttributeNameValuePairsData))]
public void GetAttributeNameValuePairs_ParsesPairsCorrectly(
string documentContent,
IEnumerable<KeyValuePair<string, string>> expectedPairs)
{
// Arrange
var errorSink = new ErrorSink();
var parseResult = ParseDocument(documentContent, errorSink);
var document = parseResult.Document;
var rewriters = RazorParser.GetDefaultRewriters(new HtmlMarkupParser());
var rewritingContext = new RewritingContext(document, errorSink);
foreach (var rewriter in rewriters)
{
rewriter.Rewrite(rewritingContext);
}
var block = rewritingContext.SyntaxTree.Children.First();
var parseTreeRewriter = new TagHelperParseTreeRewriter(provider: null);
// Assert - Guard
var tagBlock = Assert.IsType<Block>(block);
Assert.Equal(BlockType.Tag, tagBlock.Type);
Assert.Empty(errorSink.Errors);
// Act
var pairs = parseTreeRewriter.GetAttributeNameValuePairs(tagBlock);
// Assert
Assert.Equal(expectedPairs, pairs);
}
public static TheoryData PartialRequiredParentData
{
get
@ -716,7 +779,7 @@ namespace Microsoft.AspNetCore.Razor.Test.TagHelpers
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "required" },
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "required" } },
AllowedChildren = new[] { "br" }
}
};
@ -1648,21 +1711,25 @@ namespace Microsoft.AspNetCore.Razor.Test.TagHelpers
TagName = "p",
TypeName = "pTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "class" }
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } }
},
new TagHelperDescriptor
{
TagName = "div",
TypeName = "divTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "class", "style" }
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" },
new TagHelperRequiredAttributeDescriptor { Name = "style" }
}
},
new TagHelperDescriptor
{
TagName = "*",
TypeName = "catchAllTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "catchAll" }
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "catchAll" } }
}
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
@ -1911,14 +1978,14 @@ namespace Microsoft.AspNetCore.Razor.Test.TagHelpers
TagName = "p",
TypeName = "pTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "class" }
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } }
},
new TagHelperDescriptor
{
TagName = "*",
TypeName = "catchAllTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "catchAll" }
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "catchAll" } }
}
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
@ -2135,7 +2202,7 @@ namespace Microsoft.AspNetCore.Razor.Test.TagHelpers
TagName = "p",
TypeName = "pTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { "class" }
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } }
}
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);

View File

@ -0,0 +1,173 @@
using Xunit;
namespace Microsoft.AspNetCore.Razor.Compilation.TagHelpers
{
public class TagHelperRequiredAttributeDescriptorTest
{
public static TheoryData RequiredAttributeDescriptorData
{
get
{
// requiredAttributeDescriptor, attributeName, attributeValue, expectedResult
return new TheoryData<TagHelperRequiredAttributeDescriptor, string, string, bool>
{
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key"
},
"KeY",
"value",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key"
},
"keys",
"value",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "route-",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
},
"ROUTE-area",
"manage",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "route-",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
},
"routearea",
"manage",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "route-",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
},
"route-",
"manage",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
},
"KeY",
"value",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
},
"keys",
"value",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "value",
ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch,
},
"key",
"value",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "value",
ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch,
},
"key",
"Value",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "class",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "btn",
ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
},
"class",
"btn btn-success",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "class",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "btn",
ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
},
"class",
"BTN btn-success",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "href",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "#navigate",
ValueComparison = TagHelperRequiredAttributeValueComparison.SuffixMatch,
},
"href",
"/home/index#navigate",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "href",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "#navigate",
ValueComparison = TagHelperRequiredAttributeValueComparison.SuffixMatch,
},
"href",
"/home/index#NAVigate",
false
},
};
}
}
[Theory]
[MemberData(nameof(RequiredAttributeDescriptorData))]
public void Matches_ReturnsExpectedResult(
TagHelperRequiredAttributeDescriptor requiredAttributeDescriptor,
string attributeName,
string attributeValue,
bool expectedResult)
{
// Act
var result = requiredAttributeDescriptor.IsMatch(attributeName, attributeValue);
// Assert
Assert.Equal(expectedResult, result);
}
}
}

View File

@ -0,0 +1,272 @@
#pragma checksum "CssSelectorTagHelperAttributes.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "7c9072aeb0075e207732cb34646d54529eade9f0"
namespace TestOutput
{
using System;
using System.Threading.Tasks;
public class CssSelectorTagHelperAttributes
{
#line hidden
#pragma warning disable 0414
private global::Microsoft.AspNetCore.Razor.TagHelperContent __tagHelperStringValueBuffer = null;
#pragma warning restore 0414
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelperExecutionContext __tagHelperExecutionContext = null;
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelperRunner __tagHelperRunner = null;
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelperScopeManager __tagHelperScopeManager = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelperScopeManager();
private global::TestNamespace.ATagHelper __TestNamespace_ATagHelper = null;
private global::TestNamespace.CatchAllTagHelper __TestNamespace_CatchAllTagHelper = null;
private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_0 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("href", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("~/"));
private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_1 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("href", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("~/hello"));
private global::TestNamespace.ATagHelperMultipleSelectors __TestNamespace_ATagHelperMultipleSelectors = null;
private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_2 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("href", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("~/?hello=world"));
private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_3 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("href", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("~/?hello=world@false"));
private global::TestNamespace.InputTagHelper __TestNamespace_InputTagHelper = null;
private global::TestNamespace.InputTagHelper2 __TestNamespace_InputTagHelper2 = null;
private global::TestNamespace.CatchAllTagHelper2 __TestNamespace_CatchAllTagHelper2 = null;
private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_4 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("value", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("3 TagHelpers"));
private static readonly global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute __tagHelperAttribute_5 = new global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute("value", new global::Microsoft.AspNetCore.Html.HtmlEncodedString("2 TagHelper"));
#line hidden
public CssSelectorTagHelperAttributes()
{
}
#pragma warning disable 1998
public override async Task ExecuteAsync()
{
__tagHelperRunner = __tagHelperRunner ?? new global::Microsoft.AspNetCore.Razor.Runtime.TagHelperRunner();
Instrumentation.BeginContext(30, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
Instrumentation.BeginContext(45, 13, true);
WriteLiteral("2 TagHelpers.");
Instrumentation.EndContext();
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__TestNamespace_ATagHelper = CreateTagHelper<global::TestNamespace.ATagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_ATagHelper);
__TestNamespace_CatchAllTagHelper = CreateTagHelper<global::TestNamespace.CatchAllTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
__tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_0);
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
if (!__tagHelperExecutionContext.Output.IsContentModified)
{
__tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
}
Instrumentation.BeginContext(32, 30, false);
Write(__tagHelperExecutionContext.Output);
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(62, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
Instrumentation.BeginContext(80, 12, true);
WriteLiteral("1 TagHelper.");
Instrumentation.EndContext();
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__TestNamespace_CatchAllTagHelper = CreateTagHelper<global::TestNamespace.CatchAllTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
__tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_1);
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
if (!__tagHelperExecutionContext.Output.IsContentModified)
{
__tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
}
Instrumentation.BeginContext(64, 32, false);
Write(__tagHelperExecutionContext.Output);
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(96, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
Instrumentation.BeginContext(123, 12, true);
WriteLiteral("2 TagHelpers");
Instrumentation.EndContext();
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__TestNamespace_ATagHelperMultipleSelectors = CreateTagHelper<global::TestNamespace.ATagHelperMultipleSelectors>();
__tagHelperExecutionContext.Add(__TestNamespace_ATagHelperMultipleSelectors);
__TestNamespace_CatchAllTagHelper = CreateTagHelper<global::TestNamespace.CatchAllTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
__tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_2);
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
if (!__tagHelperExecutionContext.Output.IsContentModified)
{
__tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
}
Instrumentation.BeginContext(98, 41, false);
Write(__tagHelperExecutionContext.Output);
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(139, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
Instrumentation.BeginContext(172, 12, true);
WriteLiteral("2 TagHelpers");
Instrumentation.EndContext();
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__TestNamespace_ATagHelperMultipleSelectors = CreateTagHelper<global::TestNamespace.ATagHelperMultipleSelectors>();
__tagHelperExecutionContext.Add(__TestNamespace_ATagHelperMultipleSelectors);
__TestNamespace_CatchAllTagHelper = CreateTagHelper<global::TestNamespace.CatchAllTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
BeginAddHtmlAttributeValues(__tagHelperExecutionContext, "href", 3);
AddHtmlAttributeValue("", 150, "~/", 150, 2, true);
#line 6 "CssSelectorTagHelperAttributes.cshtml"
AddHtmlAttributeValue("", 152, false, 152, 6, false);
#line default
#line hidden
AddHtmlAttributeValue("", 158, "?hello=world", 158, 12, true);
EndAddHtmlAttributeValues(__tagHelperExecutionContext);
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
if (!__tagHelperExecutionContext.Output.IsContentModified)
{
__tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
}
Instrumentation.BeginContext(141, 47, false);
Write(__tagHelperExecutionContext.Output);
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(188, 35, true);
WriteLiteral("\r\n<a href=\' ~/\'>0 TagHelpers.</a>\r\n");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
Instrumentation.BeginContext(240, 11, true);
WriteLiteral("1 TagHelper");
Instrumentation.EndContext();
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__TestNamespace_CatchAllTagHelper = CreateTagHelper<global::TestNamespace.CatchAllTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
BeginAddHtmlAttributeValues(__tagHelperExecutionContext, "href", 2);
AddHtmlAttributeValue("", 231, "~/", 231, 2, true);
#line 8 "CssSelectorTagHelperAttributes.cshtml"
AddHtmlAttributeValue("", 233, false, 233, 6, false);
#line default
#line hidden
EndAddHtmlAttributeValues(__tagHelperExecutionContext);
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
if (!__tagHelperExecutionContext.Output.IsContentModified)
{
__tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
}
Instrumentation.BeginContext(223, 32, false);
Write(__tagHelperExecutionContext.Output);
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(255, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
Instrumentation.BeginContext(288, 11, true);
WriteLiteral("1 TagHelper");
Instrumentation.EndContext();
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__TestNamespace_CatchAllTagHelper = CreateTagHelper<global::TestNamespace.CatchAllTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
__tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_3);
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
if (!__tagHelperExecutionContext.Output.IsContentModified)
{
__tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
}
Instrumentation.BeginContext(257, 46, false);
Write(__tagHelperExecutionContext.Output);
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(303, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("a", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
Instrumentation.BeginContext(337, 11, true);
WriteLiteral("1 TagHelper");
Instrumentation.EndContext();
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__TestNamespace_CatchAllTagHelper = CreateTagHelper<global::TestNamespace.CatchAllTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
BeginAddHtmlAttributeValues(__tagHelperExecutionContext, "href", 2);
AddHtmlAttributeValue("", 314, "~/?hello=world", 314, 14, true);
#line 10 "CssSelectorTagHelperAttributes.cshtml"
AddHtmlAttributeValue(" ", 328, false, 329, 7, false);
#line default
#line hidden
EndAddHtmlAttributeValues(__tagHelperExecutionContext);
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
if (!__tagHelperExecutionContext.Output.IsContentModified)
{
__tagHelperExecutionContext.Output.Content = await __tagHelperExecutionContext.Output.GetChildContentAsync();
}
Instrumentation.BeginContext(305, 47, false);
Write(__tagHelperExecutionContext.Output);
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(352, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.SelfClosing, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__TestNamespace_InputTagHelper = CreateTagHelper<global::TestNamespace.InputTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_InputTagHelper);
__TestNamespace_InputTagHelper2 = CreateTagHelper<global::TestNamespace.InputTagHelper2>();
__tagHelperExecutionContext.Add(__TestNamespace_InputTagHelper2);
__TestNamespace_CatchAllTagHelper2 = CreateTagHelper<global::TestNamespace.CatchAllTagHelper2>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper2);
__TestNamespace_InputTagHelper.Type = "text";
__tagHelperExecutionContext.AddTagHelperAttribute("type", __TestNamespace_InputTagHelper.Type);
__TestNamespace_InputTagHelper2.Type = __TestNamespace_InputTagHelper.Type;
__tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_4);
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
Instrumentation.BeginContext(354, 42, false);
Write(__tagHelperExecutionContext.Output);
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(396, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.SelfClosing, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__TestNamespace_InputTagHelper2 = CreateTagHelper<global::TestNamespace.InputTagHelper2>();
__tagHelperExecutionContext.Add(__TestNamespace_InputTagHelper2);
__TestNamespace_CatchAllTagHelper2 = CreateTagHelper<global::TestNamespace.CatchAllTagHelper2>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper2);
__TestNamespace_InputTagHelper2.Type = "texty";
__tagHelperExecutionContext.AddTagHelperAttribute("type", __TestNamespace_InputTagHelper2.Type);
__tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_4);
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
Instrumentation.BeginContext(398, 43, false);
Write(__tagHelperExecutionContext.Output);
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.End();
Instrumentation.BeginContext(441, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.SelfClosing, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__TestNamespace_InputTagHelper2 = CreateTagHelper<global::TestNamespace.InputTagHelper2>();
__tagHelperExecutionContext.Add(__TestNamespace_InputTagHelper2);
__TestNamespace_CatchAllTagHelper2 = CreateTagHelper<global::TestNamespace.CatchAllTagHelper2>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper2);
__TestNamespace_InputTagHelper2.Type = "checkbox";
__tagHelperExecutionContext.AddTagHelperAttribute("type", __TestNamespace_InputTagHelper2.Type);
__tagHelperExecutionContext.AddHtmlAttribute(__tagHelperAttribute_5);
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
Instrumentation.BeginContext(443, 45, false);
Write(__tagHelperExecutionContext.Output);
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.End();
}
#pragma warning restore 1998
}
}

View File

@ -0,0 +1,13 @@
@addTagHelper "*, something"
<a href="~/">2 TagHelpers.</a>
<a href=~/hello>1 TagHelper.</a>
<a href="~/?hello=world">2 TagHelpers</a>
<a href="~/@false?hello=world">2 TagHelpers</a>
<a href=' ~/'>0 TagHelpers.</a>
<a href=~/@false>1 TagHelper</a>
<a href="~/?hello=world@false">1 TagHelper</a>
<a href='~/?hello=world @false'>1 TagHelper</a>
<input type="text" value="3 TagHelpers" />
<input type='texty' value="3 TagHelpers" />
<input type="checkbox" value="2 TagHelper" />