Add TagHelper attribute targeting.

- Transitioned HtmlElementNameAttribute into a more generic TargetElementAttribute. Targeting an HTML element can be done by attribute, tag or both.
- Updated TagHelperDescriptor to track required attributes.
- Updated TagHelperProvider to ask for provided attributes when resolving TagHelperDescriptors, this is used to apply RequiredAttributes.
- Updated TagHelperParseTreeRewriter to properly track HTML elements that coincide with a TagHelper scope based on the presence of RequiredAttributes.

#311
This commit is contained in:
N. Taylor Mullen 2015-03-01 16:26:18 -08:00
parent b3c60976a4
commit d22246f636
11 changed files with 505 additions and 198 deletions

View File

@ -75,19 +75,19 @@ namespace Microsoft.AspNet.Razor.Runtime
} }
/// <summary> /// <summary>
/// Tag name cannot be null or whitespace. /// {0} name cannot be null or whitespace.
/// </summary> /// </summary>
internal static string HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace internal static string TargetElementAttribute_NameCannotBeNullOrWhitespace
{ {
get { return GetString("HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace"); } get { return GetString("TargetElementAttribute_NameCannotBeNullOrWhitespace"); }
} }
/// <summary> /// <summary>
/// Tag name cannot be null or whitespace. /// {0} name cannot be null or whitespace.
/// </summary> /// </summary>
internal static string FormatHtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace() internal static string FormatTargetElementAttribute_NameCannotBeNullOrWhitespace(object p0)
{ {
return GetString("HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace"); return string.Format(CultureInfo.CurrentCulture, GetString("TargetElementAttribute_NameCannotBeNullOrWhitespace"), p0);
} }
/// <summary> /// <summary>
@ -123,19 +123,19 @@ namespace Microsoft.AspNet.Razor.Runtime
} }
/// <summary> /// <summary>
/// Tag helpers cannot target element name '{0}' because it contains a '{1}' character. /// Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character.
/// </summary> /// </summary>
internal static string HtmlElementNameAttribute_InvalidElementName internal static string TargetElementAttribute_InvalidName
{ {
get { return GetString("HtmlElementNameAttribute_InvalidElementName"); } get { return GetString("TargetElementAttribute_InvalidName"); }
} }
/// <summary> /// <summary>
/// Tag helpers cannot target element name '{0}' because it contains a '{1}' character. /// Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character.
/// </summary> /// </summary>
internal static string FormatHtmlElementNameAttribute_InvalidElementName(object p0, object p1) internal static string FormatTargetElementAttribute_InvalidName(object p0, object p1, object p2)
{ {
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlElementNameAttribute_InvalidElementName"), p0, p1); return string.Format(CultureInfo.CurrentCulture, GetString("TargetElementAttribute_InvalidName"), p0, p1, p2);
} }
/// <summary> /// <summary>
@ -170,6 +170,38 @@ namespace Microsoft.AspNet.Razor.Runtime
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorResolver_InvalidTagHelperPrefixValue"), p0, p1, p2); return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorResolver_InvalidTagHelperPrefixValue"), p0, p1, p2);
} }
/// <summary>
/// Attribute
/// </summary>
internal static string TagHelperDescriptorFactory_Attribute
{
get { return GetString("TagHelperDescriptorFactory_Attribute"); }
}
/// <summary>
/// Attribute
/// </summary>
internal static string FormatTagHelperDescriptorFactory_Attribute()
{
return GetString("TagHelperDescriptorFactory_Attribute");
}
/// <summary>
/// Tag
/// </summary>
internal static string TagHelperDescriptorFactory_Tag
{
get { return GetString("TagHelperDescriptorFactory_Tag"); }
}
/// <summary>
/// Tag
/// </summary>
internal static string FormatTagHelperDescriptorFactory_Tag()
{
return GetString("TagHelperDescriptorFactory_Tag");
}
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -129,8 +129,8 @@
<data name="ScopeManager_EndCannotBeCalledWithoutACallToBegin" xml:space="preserve"> <data name="ScopeManager_EndCannotBeCalledWithoutACallToBegin" xml:space="preserve">
<value>Must call '{2}.{1}' before calling '{2}.{0}'.</value> <value>Must call '{2}.{1}' before calling '{2}.{0}'.</value>
</data> </data>
<data name="HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace" xml:space="preserve"> <data name="TargetElementAttribute_NameCannotBeNullOrWhitespace" xml:space="preserve">
<value>Tag name cannot be null or whitespace.</value> <value>{0} name cannot be null or whitespace.</value>
</data> </data>
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve"> <data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
<value>The value cannot be null or empty.</value> <value>The value cannot be null or empty.</value>
@ -138,8 +138,8 @@
<data name="TagHelperDescriptorResolver_EncounteredUnexpectedError" xml:space="preserve"> <data name="TagHelperDescriptorResolver_EncounteredUnexpectedError" xml:space="preserve">
<value>Encountered an unexpected error when attempting to resolve tag helper directive '{0}' with value '{1}'. Error: {2}</value> <value>Encountered an unexpected error when attempting to resolve tag helper directive '{0}' with value '{1}'. Error: {2}</value>
</data> </data>
<data name="HtmlElementNameAttribute_InvalidElementName" xml:space="preserve"> <data name="TargetElementAttribute_InvalidName" xml:space="preserve">
<value>Tag helpers cannot target element name '{0}' because it contains a '{1}' character.</value> <value>Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character.</value>
</data> </data>
<data name="TagHelperDescriptorResolver_InvalidTagHelperDirective" xml:space="preserve"> <data name="TagHelperDescriptorResolver_InvalidTagHelperDirective" xml:space="preserve">
<value>Invalid tag helper directive '{0}'. Cannot have multiple '{0}' directives on a page.</value> <value>Invalid tag helper directive '{0}'. Cannot have multiple '{0}' directives on a page.</value>
@ -147,4 +147,10 @@
<data name="TagHelperDescriptorResolver_InvalidTagHelperPrefixValue" xml:space="preserve"> <data name="TagHelperDescriptorResolver_InvalidTagHelperPrefixValue" xml:space="preserve">
<value>Invalid tag helper directive '{0}' value. '{1} is not allowed in prefix '{2}'.</value> <value>Invalid tag helper directive '{0}' value. '{1} is not allowed in prefix '{2}'.</value>
</data> </data>
<data name="TagHelperDescriptorFactory_Attribute" xml:space="preserve">
<value>Attribute</value>
</data>
<data name="TagHelperDescriptorFactory_Tag" xml:space="preserve">
<value>Tag</value>
</data>
</root> </root>

View File

@ -1,70 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.Framework.Internal;
namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
/// <summary>
/// Used to override a <see cref="ITagHelper"/>'s default tag name target.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class HtmlElementNameAttribute : Attribute
{
/// <summary>
/// Instantiates a new instance of the <see cref="HtmlElementNameAttribute"/> class.
/// </summary>
/// <param name="tag">The HTML tag name for the <see cref="TagHelper"/> to target.</param>
public HtmlElementNameAttribute([NotNull] string tag)
{
ValidateTagName(tag, nameof(tag));
Tags = new[] { tag };
}
/// <summary>
/// Instantiates a new instance of the <see cref="HtmlElementNameAttribute"/> class.
/// </summary>
/// <param name="tag">The HTML tag name for the <see cref="TagHelper"/> to target.</param>
/// <param name="additionalTags">Additional HTML tag names for the <see cref="TagHelper"/> to target.</param>
public HtmlElementNameAttribute([NotNull] string tag, [NotNull] params string[] additionalTags)
{
ValidateTagName(tag, nameof(tag));
foreach (var tagName in additionalTags)
{
ValidateTagName(tagName, nameof(additionalTags));
}
var allTags = new List<string>(additionalTags);
allTags.Add(tag);
Tags = allTags;
}
/// <summary>
/// An <see cref="IEnumerable{string}"/> of tag names for the <see cref="TagHelper"/> to target.
/// </summary>
public IEnumerable<string> Tags { get; }
private static void ValidateTagName(string tagName, string parameterName)
{
if (string.IsNullOrWhiteSpace(tagName))
{
throw new ArgumentException(
Resources.HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace,
parameterName);
}
if (tagName.Contains('!'))
{
throw new ArgumentException(
Resources.FormatHtmlElementNameAttribute_InvalidElementName(tagName, '!'),
parameterName);
}
}
}
}

View File

@ -6,7 +6,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.AspNet.Razor.Parser;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.AspNet.Razor.Text;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Razor.Runtime.TagHelpers namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
@ -28,34 +31,52 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
// TODO: Investigate if we should cache TagHelperDescriptors for types: // TODO: Investigate if we should cache TagHelperDescriptors for types:
// https://github.com/aspnet/Razor/issues/165 // https://github.com/aspnet/Razor/issues/165
public static ICollection<char> InvalidNonWhitespaceNameCharacters { get; } = new HashSet<char>(
new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'' });
/// <summary> /// <summary>
/// Creates a <see cref="TagHelperDescriptor"/> from the given <paramref name="type"/>. /// Creates a <see cref="TagHelperDescriptor"/> from the given <paramref name="type"/>.
/// </summary> /// </summary>
/// <param name="assemblyName">The assembly name that contains <paramref name="type"/>.</param> /// <param name="assemblyName">The assembly name that contains <paramref name="type"/>.</param>
/// <param name="type">The type to create a <see cref="TagHelperDescriptor"/> from.</param> /// <param name="type">The type to create a <see cref="TagHelperDescriptor"/> from.</param>
/// <returns>A <see cref="TagHelperDescriptor"/> that describes the given <paramref name="type"/>.</returns> /// <returns>A <see cref="TagHelperDescriptor"/> that describes the given <paramref name="type"/>.</returns>
public static IEnumerable<TagHelperDescriptor> CreateDescriptors(string assemblyName, Type type) public static IEnumerable<TagHelperDescriptor> CreateDescriptors(
string assemblyName,
[NotNull] Type type,
[NotNull] ParserErrorSink errorSink)
{ {
var tagNames = GetTagNames(type); var typeInfo = type.GetTypeInfo();
var typeName = type.FullName;
var attributeDescriptors = GetAttributeDescriptors(type); var attributeDescriptors = GetAttributeDescriptors(type);
var targetElementAttributes = GetValidTargetElementAttributes(typeInfo, errorSink);
var tagHelperDescriptors =
BuildTagHelperDescriptors(
typeInfo,
assemblyName,
attributeDescriptors,
targetElementAttributes);
return tagNames.Select(tagName => return tagHelperDescriptors.Distinct(TagHelperDescriptorComparer.Default);
new TagHelperDescriptor(
prefix: string.Empty,
tagName: tagName,
typeName: typeName,
assemblyName: assemblyName,
attributes: attributeDescriptors));
} }
private static IEnumerable<string> GetTagNames(Type tagHelperType) private static IEnumerable<TargetElementAttribute> GetValidTargetElementAttributes(
TypeInfo typeInfo,
ParserErrorSink errorSink)
{ {
var typeInfo = tagHelperType.GetTypeInfo(); var targetElementAttributes = typeInfo.GetCustomAttributes<TargetElementAttribute>(inherit: false);
var attributes = typeInfo.GetCustomAttributes<HtmlElementNameAttribute>(inherit: false);
return targetElementAttributes.Where(attribute => ValidTargetElementAttributeNames(attribute, errorSink));
}
private static IEnumerable<TagHelperDescriptor> BuildTagHelperDescriptors(
TypeInfo typeInfo,
string assemblyName,
IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,
IEnumerable<TargetElementAttribute> targetElementAttributes)
{
var typeName = typeInfo.FullName;
// If there isn't an attribute specifying the tag name derive it from the name // If there isn't an attribute specifying the tag name derive it from the name
if (!attributes.Any()) if (!targetElementAttributes.Any())
{ {
var name = typeInfo.Name; var name = typeInfo.Name;
@ -64,11 +85,122 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
name = name.Substring(0, name.Length - TagHelperNameEnding.Length); name = name.Substring(0, name.Length - TagHelperNameEnding.Length);
} }
return new[] { ToHtmlCase(name) }; return new[]
{
BuildTagHelperDescriptor(
ToHtmlCase(name),
typeName,
assemblyName,
attributeDescriptors,
requiredAttributes: Enumerable.Empty<string>())
};
} }
// Remove duplicate tag names. return targetElementAttributes.Select(
return attributes.SelectMany(attribute => attribute.Tags).Distinct(); attribute => BuildTagHelperDescriptor(typeName, assemblyName, attributeDescriptors, attribute));
}
private static TagHelperDescriptor BuildTagHelperDescriptor(
string typeName,
string assemblyName,
IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,
TargetElementAttribute targetElementAttribute)
{
var requiredAttributes = GetCommaSeparatedValues(targetElementAttribute.Attributes);
return BuildTagHelperDescriptor(
targetElementAttribute.Tag,
typeName,
assemblyName,
attributeDescriptors,
requiredAttributes);
}
private static TagHelperDescriptor BuildTagHelperDescriptor(
string tagName,
string typeName,
string assemblyName,
IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,
IEnumerable<string> requiredAttributes)
{
return new TagHelperDescriptor(
prefix: string.Empty,
tagName: tagName,
typeName: typeName,
assemblyName: assemblyName,
attributes: attributeDescriptors,
requiredAttributes: requiredAttributes);
}
/// <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>
internal static bool ValidTargetElementAttributeNames(
TargetElementAttribute attribute,
ParserErrorSink 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;
}
}
return validTagName && validAttributeNames;
}
private static bool ValidateName(
string name,
bool targetingAttributes,
ParserErrorSink errorSink)
{
var targetName = targetingAttributes ?
Resources.TagHelperDescriptorFactory_Attribute :
Resources.TagHelperDescriptorFactory_Tag;
var validName = true;
if (string.IsNullOrWhiteSpace(name))
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTargetElementAttribute_NameCannotBeNullOrWhitespace(targetName));
validName = false;
}
else
{
foreach (var character in name)
{
if (char.IsWhiteSpace(character) ||
InvalidNonWhitespaceNameCharacters.Contains(character))
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTargetElementAttribute_InvalidName(
targetName.ToLower(),
name,
character));
validName = false;
}
}
}
return validName;
} }
private static IEnumerable<TagHelperAttributeDescriptor> GetAttributeDescriptors(Type type) private static IEnumerable<TagHelperAttributeDescriptor> GetAttributeDescriptors(Type type)

View File

@ -25,8 +25,6 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ TagHelperDirectiveType.RemoveTagHelper, SyntaxConstants.CSharp.RemoveTagHelperKeyword }, { TagHelperDirectiveType.RemoveTagHelper, SyntaxConstants.CSharp.RemoveTagHelperKeyword },
{ TagHelperDirectiveType.TagHelperPrefix, SyntaxConstants.CSharp.TagHelperPrefixKeyword }, { TagHelperDirectiveType.TagHelperPrefix, SyntaxConstants.CSharp.TagHelperPrefixKeyword },
}; };
private static readonly HashSet<char> InvalidNonWhitespacePrefixCharacters =
new HashSet<char>(new[] { '@', '!', '<', '!', '/', '?', '[', '>', ']', '=', '"', '\'' });
private readonly TagHelperTypeResolver _typeResolver; private readonly TagHelperTypeResolver _typeResolver;
@ -131,7 +129,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
// Convert types to TagHelperDescriptors // Convert types to TagHelperDescriptors
var descriptors = tagHelperTypes.SelectMany( var descriptors = tagHelperTypes.SelectMany(
type => TagHelperDescriptorFactory.CreateDescriptors(assemblyName, type)); type => TagHelperDescriptorFactory.CreateDescriptors(assemblyName, type, errorSink));
return descriptors; return descriptors;
} }
@ -150,7 +148,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
descriptor.TagName, descriptor.TagName,
descriptor.TypeName, descriptor.TypeName,
descriptor.AssemblyName, descriptor.AssemblyName,
descriptor.Attributes)); descriptor.Attributes,
descriptor.RequiredAttributes));
} }
return descriptors; return descriptors;
@ -198,7 +197,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
// Prefixes are correlated with tag names, tag names cannot have whitespace. // Prefixes are correlated with tag names, tag names cannot have whitespace.
if (char.IsWhiteSpace(character) || if (char.IsWhiteSpace(character) ||
InvalidNonWhitespacePrefixCharacters.Contains(character)) TagHelperDescriptorFactory.InvalidNonWhitespaceNameCharacters.Contains(character))
{ {
errorSink.OnError( errorSink.OnError(
directiveLocation, directiveLocation,

View File

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
/// <summary>
/// Provides an <see cref="ITagHelper"/>'s target.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class TargetElementAttribute : Attribute
{
public const string CatchAllDescriptorTarget = TagHelperDescriptorProvider.CatchAllDescriptorTarget;
/// <summary>
/// Instantiates a new instance of the <see cref="TargetElementAttribute"/> class with <see cref="Tag"/>
/// set to <c>*</c>.
/// </summary>
/// <remarks>A <c>*</c> <see cref="Tag"/> value indicates an <see cref="ITagHelper"/>
/// that targets all HTML elements with the required <see cref="Attributes"/>.</remarks>
public TargetElementAttribute()
: this(CatchAllDescriptorTarget)
{
}
/// <summary>
/// Instantiates a new instance of the <see cref="TargetElementAttribute"/> class.
/// </summary>
/// <param name="tag">
/// The HTML tag the <see cref="ITagHelper"/> targets.
/// </param>
/// <remarks>A <c>*</c> <paramref name="tag"/> value indicates an <see cref="ITagHelper"/>
/// that targets all HTML elements with the required <see cref="Attributes"/>.</remarks>
public TargetElementAttribute(string tag)
{
Tag = tag;
}
/// <summary>
/// The HTML tag the <see cref="ITagHelper"/> targets.
/// </summary>
/// <remarks>A <c>*</c> <see cref="Tag"/> value indicates an <see cref="ITagHelper"/>
/// that targets all HTML elements with the required <see cref="Attributes"/>.</remarks>
public string Tag { get; }
/// <summary>
/// A comma-separated <see cref="string"/> of attributes the HTML element must contain for the
/// <see cref="ITagHelper"/> to run.
/// </summary>
public string Attributes { get; set; }
}
}

View File

@ -224,9 +224,16 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
if (name == null) if (name == null)
{ {
errorSink.OnError(span.Start, // We couldn't find a name, if the original span content was whitespace it ultimately means the tag
RazorResources.TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed, // that owns this "attribute" is malformed and is expecting a user to type a new attribute.
span.Content.Length); // ex: <myTH class="btn"| |
if (!string.IsNullOrWhiteSpace(span.Content))
{
errorSink.OnError(
span.Start,
RazorResources.TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed,
span.Content.Length);
}
attribute = default(KeyValuePair<string, SyntaxTreeNode>); attribute = default(KeyValuePair<string, SyntaxTreeNode>);

View File

@ -14,14 +14,14 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
public class TagHelperParseTreeRewriter : ISyntaxTreeRewriter public class TagHelperParseTreeRewriter : ISyntaxTreeRewriter
{ {
private TagHelperDescriptorProvider _provider; private TagHelperDescriptorProvider _provider;
private Stack<TagHelperBlockBuilder> _tagStack; private Stack<TagHelperBlockTracker> _trackerStack;
private Stack<BlockBuilder> _blockStack; private Stack<BlockBuilder> _blockStack;
private BlockBuilder _currentBlock; private BlockBuilder _currentBlock;
public TagHelperParseTreeRewriter(TagHelperDescriptorProvider provider) public TagHelperParseTreeRewriter(TagHelperDescriptorProvider provider)
{ {
_provider = provider; _provider = provider;
_tagStack = new Stack<TagHelperBlockBuilder>(); _trackerStack = new Stack<TagHelperBlockTracker>();
_blockStack = new Stack<BlockBuilder>(); _blockStack = new Stack<BlockBuilder>();
} }
@ -41,7 +41,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
CodeGenerator = input.CodeGenerator CodeGenerator = input.CodeGenerator
}); });
var activeTagHelpers = _tagStack.Count; var activeTagHelpers = _trackerStack.Count;
foreach (var child in input.Children) foreach (var child in input.Children)
{ {
@ -76,14 +76,14 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
// We captured the number of active tag helpers at the start of our logic, it should be the same. If not // We captured the number of active tag helpers at the start of our logic, it should be the same. If not
// it means that there are malformed tag helpers at the top of our stack. // it means that there are malformed tag helpers at the top of our stack.
if (activeTagHelpers != _tagStack.Count) if (activeTagHelpers != _trackerStack.Count)
{ {
// Malformed tag helpers built here will be tag helpers that do not have end tags in the current block // Malformed tag helpers built here will be tag helpers that do not have end tags in the current block
// scope. Block scopes are special cases in Razor such as @<p> would cause an error because there's no // scope. Block scopes are special cases in Razor such as @<p> would cause an error because there's no
// matching end </p> tag in the template block scope and therefore doesn't make sense as a tag helper. // matching end </p> tag in the template block scope and therefore doesn't make sense as a tag helper.
BuildMalformedTagHelpers(_tagStack.Count - activeTagHelpers, context); BuildMalformedTagHelpers(_trackerStack.Count - activeTagHelpers, context);
Debug.Assert(activeTagHelpers == _tagStack.Count); Debug.Assert(activeTagHelpers == _trackerStack.Count);
} }
BuildCurrentlyTrackedBlock(); BuildCurrentlyTrackedBlock();
@ -91,8 +91,6 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
private bool TryRewriteTagHelper(Block tagBlock, RewritingContext context) private bool TryRewriteTagHelper(Block tagBlock, RewritingContext context)
{ {
// TODO: Fully handle malformed tags: https://github.com/aspnet/Razor/issues/104
// Get tag name of the current block (doesn't matter if it's an end or start tag) // Get tag name of the current block (doesn't matter if it's an end or start tag)
var tagName = GetTagName(tagBlock); var tagName = GetTagName(tagBlock);
@ -104,22 +102,39 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
var descriptors = Enumerable.Empty<TagHelperDescriptor>(); var descriptors = Enumerable.Empty<TagHelperDescriptor>();
if (IsPotentialTagHelper(tagName, tagBlock)) if (!IsPotentialTagHelper(tagName, tagBlock))
{
descriptors = _provider.GetTagHelpers(tagName);
}
// If there aren't any TagHelperDescriptors registered then we aren't a TagHelper
if (!descriptors.Any())
{ {
return false; return false;
} }
var tracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null;
var tagNameScope = tracker?.Builder.TagName ?? string.Empty;
if (!IsEndTag(tagBlock)) if (!IsEndTag(tagBlock))
{ {
// We're in a begin tag helper block // 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 validTagStructure = ValidTagStructure(tagName, tagBlock, context); descriptors = _provider.GetDescriptors(tagName, providedAttributes);
// If there aren't any TagHelperDescriptors registered then we aren't a TagHelper
if (!descriptors.Any())
{
// If the current tag matches the current TagHelper scope it means the parent TagHelper matched
// all the required attributes but the current one did not; therefore, we need to increment the
// OpenMatchingTags counter for current the TagHelperBlock so we don't end it too early.
// ex: <myth req="..."><myth></myth></myth> We don't want the first myth to close on the inside
// tag.
if (string.Equals(tagNameScope, tagName, StringComparison.OrdinalIgnoreCase))
{
tracker.OpenMatchingTags++;
}
return false;
}
// We're in a start TagHelper block.
var validTagStructure = ValidateTagStructure(tagName, tagBlock, context);
var builder = TagHelperBlockRewriter.Rewrite( var builder = TagHelperBlockRewriter.Rewrite(
tagName, tagName,
@ -144,32 +159,43 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
} }
else else
{ {
// We're in an end tag helper block. // Validate that our end tag matches the currently scoped tag, if not we may need to error.
var tagNameScope = _tagStack.Count > 0 ? _tagStack.Peek().TagName : string.Empty;
// Validate that our end tag helper matches the currently scoped tag helper, if not we
// need to error.
if (tagNameScope.Equals(tagName, StringComparison.OrdinalIgnoreCase)) if (tagNameScope.Equals(tagName, StringComparison.OrdinalIgnoreCase))
{ {
ValidTagStructure(tagName, tagBlock, context); // If there are additional end tags required before we can build our block it means we're in a
// situation like this: <myth req="..."><myth></myth></myth> where we're at the inside </myth>.
if (tracker.OpenMatchingTags > 0)
{
tracker.OpenMatchingTags--;
return false;
}
ValidateTagStructure(tagName, tagBlock, context);
BuildCurrentlyTrackedTagHelperBlock(tagBlock); BuildCurrentlyTrackedTagHelperBlock(tagBlock);
} }
else else
{ {
// If there are not TagHelperDescriptors associated with the end tag block that also have no
// required attributes then it means we can't be a TagHelper, bail out.
if (!_provider.GetDescriptors(tagName, attributeNames: Enumerable.Empty<string>()).Any())
{
return false;
}
// Current tag helper scope does not match the end tag. Attempt to recover the tag // Current tag helper scope does not match the end tag. Attempt to recover the tag
// helper by looking up the previous tag helper scopes for a matching tag. If we // helper by looking up the previous tag helper scopes for a matching tag. If we
// can't recover it means there was no corresponding tag helper begin tag. // can't recover it means there was no corresponding tag helper start tag.
if (TryRecoverTagHelper(tagName, tagBlock, context)) if (TryRecoverTagHelper(tagName, tagBlock, context))
{ {
ValidTagStructure(tagName, tagBlock, context); ValidateTagStructure(tagName, tagBlock, context);
// Successfully recovered, move onto the next element. // Successfully recovered, move onto the next element.
} }
else else
{ {
// Could not recover, the end tag helper has no corresponding begin tag, create // Could not recover, the end tag helper has no corresponding start tag, create
// an error based on the current childBlock. // an error based on the current childBlock.
context.ErrorSink.OnError( context.ErrorSink.OnError(
tagBlock.Start, tagBlock.Start,
@ -183,13 +209,61 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
return true; return true;
} }
private static bool ValidTagStructure(string tagName, Block tag, RewritingContext context) private IEnumerable<string> GetAttributeNames(Block tagBlock)
{
// Need to calculate how many children we should take that represent the attributes.
var childrenOffset = IsPartialTag(tagBlock) ? 1 : 2;
var attributeChildren = tagBlock.Children.Skip(1).Take(tagBlock.Children.Count() - childrenOffset);
var attributeNames = new List<string>();
foreach (var child in attributeChildren)
{
Span childSpan;
if (child.IsBlock)
{
childSpan = ((Block)child).FindFirstDescendentSpan();
if (childSpan == null)
{
continue;
}
}
else
{
childSpan = child as Span;
}
var attributeName = childSpan
.Content
.Split(separator: new[] { '=' }, count: 2)[0]
.TrimStart();
attributeNames.Add(attributeName);
}
return attributeNames;
}
private static bool ValidateTagStructure(string tagName, Block tag, RewritingContext context)
{ {
// We assume an invalid structure until we verify that the tag meets all of our "valid structure" criteria. // We assume an invalid structure until we verify that the tag meets all of our "valid structure" criteria.
var invalidStructure = true; if (IsPartialTag(tag))
{
context.ErrorSink.OnError(
tag.Start,
RazorResources.FormatTagHelpersParseTreeRewriter_MissingCloseAngle(tagName));
return false;
}
return true;
}
private static bool IsPartialTag(Block tagBlock)
{
// No need to validate the tag end because in order to be a tag block it must start with '<'. // No need to validate the tag end because in order to be a tag block it must start with '<'.
var tagEnd = tag.Children.Last() as Span; var tagEnd = tagBlock.Children.Last() as Span;
// If our tag end is not a markup span it means it's some sort of code SyntaxTreeNode (not a valid format) // If our tag end is not a markup span it means it's some sort of code SyntaxTreeNode (not a valid format)
if (tagEnd != null && tagEnd.Kind == SpanKind.Markup) if (tagEnd != null && tagEnd.Kind == SpanKind.Markup)
@ -198,18 +272,11 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
if (endSymbol != null && endSymbol.Type == HtmlSymbolType.CloseAngle) if (endSymbol != null && endSymbol.Type == HtmlSymbolType.CloseAngle)
{ {
invalidStructure = false; return false;
} }
} }
if (invalidStructure) return true;
{
context.ErrorSink.OnError(
tag.Start,
RazorResources.FormatTagHelpersParseTreeRewriter_MissingCloseAngle(tagName));
}
return !invalidStructure;
} }
private void BuildCurrentlyTrackedBlock() private void BuildCurrentlyTrackedBlock()
@ -239,7 +306,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
{ {
// Track the original end tag so the editor knows where each piece of the TagHelperBlock lies // Track the original end tag so the editor knows where each piece of the TagHelperBlock lies
// for formatting. // for formatting.
_tagStack.Pop().SourceEndTag = endTag; _trackerStack.Pop().Builder.SourceEndTag = endTag;
BuildCurrentlyTrackedBlock(); BuildCurrentlyTrackedBlock();
} }
@ -256,11 +323,6 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
childSpan.Kind != SpanKind.Transition; childSpan.Kind != SpanKind.Transition;
} }
private bool IsRegisteredTagHelper(string tagName)
{
return _provider.GetTagHelpers(tagName).Any();
}
private void TrackBlock(BlockBuilder builder) private void TrackBlock(BlockBuilder builder)
{ {
_currentBlock = builder; _currentBlock = builder;
@ -270,7 +332,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
private void TrackTagHelperBlock(TagHelperBlockBuilder builder) private void TrackTagHelperBlock(TagHelperBlockBuilder builder)
{ {
_tagStack.Push(builder); _trackerStack.Push(new TagHelperBlockTracker(builder));
TrackBlock(builder); TrackBlock(builder);
} }
@ -279,9 +341,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
{ {
var malformedTagHelperCount = 0; var malformedTagHelperCount = 0;
foreach (var tag in _tagStack) foreach (var tracker in _trackerStack)
{ {
if (tag.TagName.Equals(tagName, StringComparison.OrdinalIgnoreCase)) if (tracker.Builder.TagName.Equals(tagName, StringComparison.OrdinalIgnoreCase))
{ {
break; break;
} }
@ -289,9 +351,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
malformedTagHelperCount++; malformedTagHelperCount++;
} }
// If the malformedTagHelperCount == _tagStack.Count it means we couldn't find a begin tag for the tag // If the malformedTagHelperCount == _tagStack.Count it means we couldn't find a start tag for the tag
// helper, can't recover. // helper, can't recover.
if (malformedTagHelperCount != _tagStack.Count) if (malformedTagHelperCount != _trackerStack.Count)
{ {
BuildMalformedTagHelpers(malformedTagHelperCount, context); BuildMalformedTagHelpers(malformedTagHelperCount, context);
@ -302,7 +364,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
return true; return true;
} }
// Could not recover tag helper. Aka we found a tag helper end tag without a corresponding begin tag. // Could not recover tag helper. Aka we found a tag helper end tag without a corresponding start tag.
return false; return false;
} }
@ -310,7 +372,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
{ {
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
var malformedTagHelper = _tagStack.Peek(); var malformedTagHelper = _trackerStack.Peek().Builder;
context.ErrorSink.OnError( context.ErrorSink.OnError(
malformedTagHelper.Start, malformedTagHelper.Start,
@ -357,5 +419,17 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
Debug.Assert(tagBlock.Type == BlockType.Tag); Debug.Assert(tagBlock.Type == BlockType.Tag);
Debug.Assert(tagBlock.Children.First() is Span); Debug.Assert(tagBlock.Children.First() is Span);
} }
private class TagHelperBlockTracker
{
public TagHelperBlockTracker(TagHelperBlockBuilder builder)
{
Builder = builder;
}
public TagHelperBlockBuilder Builder { get; }
public uint OpenMatchingTags { get; set; }
}
} }
} }

View File

@ -14,9 +14,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <summary> /// <summary>
/// Internal for testing. /// Internal for testing.
/// </summary> /// </summary>
internal TagHelperDescriptor([NotNull] string tagName, internal TagHelperDescriptor(
[NotNull] string typeName, [NotNull] string tagName,
[NotNull] string assemblyName) [NotNull] string typeName,
[NotNull] string assemblyName)
: this( : this(
tagName, tagName,
typeName, typeName,
@ -33,12 +34,31 @@ namespace Microsoft.AspNet.Razor.TagHelpers
[NotNull] string typeName, [NotNull] string typeName,
[NotNull] string assemblyName, [NotNull] string assemblyName,
[NotNull] IEnumerable<TagHelperAttributeDescriptor> attributes) [NotNull] IEnumerable<TagHelperAttributeDescriptor> attributes)
: this(
tagName,
typeName,
assemblyName,
attributes,
requiredAttributes: Enumerable.Empty<string>())
{
}
/// <summary>
/// Internal for testing.
/// </summary>
internal TagHelperDescriptor(
[NotNull] string tagName,
[NotNull] string typeName,
[NotNull] string assemblyName,
[NotNull] IEnumerable<TagHelperAttributeDescriptor> attributes,
[NotNull] IEnumerable<string> requiredAttributes)
: this( : this(
prefix: string.Empty, prefix: string.Empty,
tagName: tagName, tagName: tagName,
typeName: typeName, typeName: typeName,
assemblyName: assemblyName, assemblyName: assemblyName,
attributes: attributes) attributes: attributes,
requiredAttributes: requiredAttributes)
{ {
} }
@ -57,12 +77,16 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <param name="attributes"> /// <param name="attributes">
/// The <see cref="TagHelperAttributeDescriptor"/>s to request from the HTML tag. /// The <see cref="TagHelperAttributeDescriptor"/>s to request from the HTML tag.
/// </param> /// </param>
/// <param name="requiredAttributes">
/// The attribute names required for the tag helper to target the HTML tag.
/// </param>
public TagHelperDescriptor( public TagHelperDescriptor(
string prefix, string prefix,
[NotNull] string tagName, [NotNull] string tagName,
[NotNull] string typeName, [NotNull] string typeName,
[NotNull] string assemblyName, [NotNull] string assemblyName,
[NotNull] IEnumerable<TagHelperAttributeDescriptor> attributes) [NotNull] IEnumerable<TagHelperAttributeDescriptor> attributes,
[NotNull] IEnumerable<string> requiredAttributes)
{ {
Prefix = prefix ?? string.Empty; Prefix = prefix ?? string.Empty;
TagName = tagName; TagName = tagName;
@ -70,6 +94,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
TypeName = typeName; TypeName = typeName;
AssemblyName = assemblyName; AssemblyName = assemblyName;
Attributes = new List<TagHelperAttributeDescriptor>(attributes); Attributes = new List<TagHelperAttributeDescriptor>(attributes);
RequiredAttributes = new List<string>(requiredAttributes);
} }
/// <summary> /// <summary>
@ -102,6 +127,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <summary> /// <summary>
/// The list of attributes the tag helper expects. /// The list of attributes the tag helper expects.
/// </summary> /// </summary>
public virtual List<TagHelperAttributeDescriptor> Attributes { get; private set; } public IList<TagHelperAttributeDescriptor> Attributes { get; private set; }
/// <summary>
/// The list of required attribute names the tag helper expects to target an element.
/// </summary>
public IList<string> RequiredAttributes { get; private set; }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.Internal.Web.Utils; using Microsoft.Internal.Web.Utils;
namespace Microsoft.AspNet.Razor.TagHelpers namespace Microsoft.AspNet.Razor.TagHelpers
@ -29,15 +30,22 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <c>false</c> otherwise.</returns> /// <c>false</c> otherwise.</returns>
/// <remarks> /// <remarks>
/// Determines equality based on <see cref="TagHelperDescriptor.TypeName"/>, /// Determines equality based on <see cref="TagHelperDescriptor.TypeName"/>,
/// <see cref="TagHelperDescriptor.AssemblyName"/>, <see cref="TagHelperDescriptor.TagName"/> and /// <see cref="TagHelperDescriptor.AssemblyName"/>, <see cref="TagHelperDescriptor.TagName"/>,
/// <see cref="TagHelperDescriptor.Prefix"/>. /// and <see cref="TagHelperDescriptor.RequiredAttributes"/>.
/// </remarks> /// </remarks>
public bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY) public bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY)
{ {
return string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal) && return string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal) &&
string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.OrdinalIgnoreCase) && string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(descriptorX.Prefix, descriptorY.Prefix, StringComparison.OrdinalIgnoreCase) && string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal) &&
string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal); Enumerable.SequenceEqual(
descriptorX.RequiredAttributes.OrderBy(
attribute => attribute,
StringComparer.OrdinalIgnoreCase),
descriptorY.RequiredAttributes.OrderBy(
attribute => attribute,
StringComparer.OrdinalIgnoreCase),
StringComparer.OrdinalIgnoreCase);
} }
/// <summary> /// <summary>
@ -47,11 +55,22 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <returns>An <see cref="int"/> that uniquely identifies the given <paramref name="descriptor"/>.</returns> /// <returns>An <see cref="int"/> that uniquely identifies the given <paramref name="descriptor"/>.</returns>
public int GetHashCode(TagHelperDescriptor descriptor) public int GetHashCode(TagHelperDescriptor descriptor)
{ {
return HashCodeCombiner.Start() var hashCodeCombiner = HashCodeCombiner
.Add(descriptor.TagName, StringComparer.OrdinalIgnoreCase) .Start()
.Add(descriptor.TypeName, StringComparer.Ordinal) .Add(descriptor.TypeName, StringComparer.Ordinal)
.Add(descriptor.AssemblyName, StringComparer.Ordinal) .Add(descriptor.TagName, StringComparer.OrdinalIgnoreCase)
.CombinedHash; .Add(descriptor.AssemblyName, StringComparer.Ordinal);
var attributes = descriptor.RequiredAttributes.OrderBy(
attribute => attribute,
StringComparer.OrdinalIgnoreCase);
foreach (var attribute in attributes)
{
hashCodeCombiner.Add(attributes);
}
return hashCodeCombiner.CombinedHash;
} }
} }
} }

View File

@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// </summary> /// </summary>
public class TagHelperDescriptorProvider public class TagHelperDescriptorProvider
{ {
private const string CatchAllDescriptorTarget = "*"; public const string CatchAllDescriptorTarget = "*";
private IDictionary<string, HashSet<TagHelperDescriptor>> _registrations; private IDictionary<string, HashSet<TagHelperDescriptor>> _registrations;
private string _tagHelperPrefix; private string _tagHelperPrefix;
@ -37,13 +37,12 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// </summary> /// </summary>
/// <param name="tagName">The name of the HTML tag to match. Providing a '*' tag name /// <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> /// 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>
/// <returns><see cref="TagHelperDescriptor"/>s that apply to the given <paramref name="tagName"/>. /// <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 /// Will return an empty <see cref="Enumerable" /> if no <see cref="TagHelperDescriptor"/>s are
/// found.</returns> /// found.</returns>
public IEnumerable<TagHelperDescriptor> GetTagHelpers(string tagName) public IEnumerable<TagHelperDescriptor> GetDescriptors(string tagName, IEnumerable<string> attributeNames)
{ {
HashSet<TagHelperDescriptor> descriptors;
if (!string.IsNullOrEmpty(_tagHelperPrefix) && if (!string.IsNullOrEmpty(_tagHelperPrefix) &&
(tagName.Length <= _tagHelperPrefix.Length || (tagName.Length <= _tagHelperPrefix.Length ||
!tagName.StartsWith(_tagHelperPrefix, StringComparison.OrdinalIgnoreCase))) !tagName.StartsWith(_tagHelperPrefix, StringComparison.OrdinalIgnoreCase)))
@ -52,29 +51,54 @@ namespace Microsoft.AspNet.Razor.TagHelpers
return Enumerable.Empty<TagHelperDescriptor>(); return Enumerable.Empty<TagHelperDescriptor>();
} }
HashSet<TagHelperDescriptor> catchAllDescriptors;
IEnumerable<TagHelperDescriptor> descriptors;
// Ensure there's a HashSet to use. // Ensure there's a HashSet to use.
if (!_registrations.TryGetValue(CatchAllDescriptorTarget, out descriptors)) if (!_registrations.TryGetValue(CatchAllDescriptorTarget, out catchAllDescriptors))
{ {
descriptors = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default); descriptors = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
} }
else
// If the requested tag name is the catch-all target, we should short circuit.
if (tagName.Equals(CatchAllDescriptorTarget, StringComparison.OrdinalIgnoreCase))
{ {
return descriptors; descriptors = catchAllDescriptors;
} }
// If we have a tag name associated with the requested name, return the descriptors + // If the requested tag name is the catch-all target, we shouldn't do the work of concatenating extra
// all of the catch-all descriptors. // descriptors.
HashSet<TagHelperDescriptor> matchingDescriptors; if (!tagName.Equals(CatchAllDescriptorTarget, StringComparison.OrdinalIgnoreCase))
if (_registrations.TryGetValue(tagName, out matchingDescriptors))
{ {
return matchingDescriptors.Concat(descriptors); // If we have a tag name associated with the requested name, we need to combine matchingDescriptors
// with all the catch-all descriptors.
HashSet<TagHelperDescriptor> matchingDescriptors;
if (_registrations.TryGetValue(tagName, out matchingDescriptors))
{
descriptors = matchingDescriptors.Concat(descriptors);
}
} }
// We couldn't any descriptors associated with the requested tag name, return all var applicableDescriptors = ApplyRequiredAttributes(descriptors, attributeNames);
// of the "catch-all" tag descriptors (there may not be any).
return descriptors; return applicableDescriptors;
}
private IEnumerable<TagHelperDescriptor> ApplyRequiredAttributes(
IEnumerable<TagHelperDescriptor> descriptors,
IEnumerable<string> attributeNames)
{
return descriptors.Where(
descriptor =>
{
foreach (var requiredAttribute in descriptor.RequiredAttributes)
{
if (!attributeNames.Contains(requiredAttribute, StringComparer.OrdinalIgnoreCase))
{
return false;
}
}
return true;
});
} }
private void Register(TagHelperDescriptor descriptor) private void Register(TagHelperDescriptor descriptor)