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:
parent
b3c60976a4
commit
d22246f636
|
|
@ -75,19 +75,19 @@ namespace Microsoft.AspNet.Razor.Runtime
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tag name cannot be null or whitespace.
|
||||
/// {0} name cannot be null or whitespace.
|
||||
/// </summary>
|
||||
internal static string HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace
|
||||
internal static string TargetElementAttribute_NameCannotBeNullOrWhitespace
|
||||
{
|
||||
get { return GetString("HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace"); }
|
||||
get { return GetString("TargetElementAttribute_NameCannotBeNullOrWhitespace"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tag name cannot be null or whitespace.
|
||||
/// {0} name cannot be null or whitespace.
|
||||
/// </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>
|
||||
|
|
@ -123,19 +123,19 @@ namespace Microsoft.AspNet.Razor.Runtime
|
|||
}
|
||||
|
||||
/// <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>
|
||||
internal static string HtmlElementNameAttribute_InvalidElementName
|
||||
internal static string TargetElementAttribute_InvalidName
|
||||
{
|
||||
get { return GetString("HtmlElementNameAttribute_InvalidElementName"); }
|
||||
get { return GetString("TargetElementAttribute_InvalidName"); }
|
||||
}
|
||||
|
||||
/// <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>
|
||||
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>
|
||||
|
|
@ -170,6 +170,38 @@ namespace Microsoft.AspNet.Razor.Runtime
|
|||
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)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -129,8 +129,8 @@
|
|||
<data name="ScopeManager_EndCannotBeCalledWithoutACallToBegin" xml:space="preserve">
|
||||
<value>Must call '{2}.{1}' before calling '{2}.{0}'.</value>
|
||||
</data>
|
||||
<data name="HtmlElementNameAttribute_ElementNameCannotBeNullOrWhitespace" xml:space="preserve">
|
||||
<value>Tag name cannot be null or whitespace.</value>
|
||||
<data name="TargetElementAttribute_NameCannotBeNullOrWhitespace" xml:space="preserve">
|
||||
<value>{0} name cannot be null or whitespace.</value>
|
||||
</data>
|
||||
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
|
||||
<value>The value cannot be null or empty.</value>
|
||||
|
|
@ -138,8 +138,8 @@
|
|||
<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>
|
||||
</data>
|
||||
<data name="HtmlElementNameAttribute_InvalidElementName" xml:space="preserve">
|
||||
<value>Tag helpers cannot target element name '{0}' because it contains a '{1}' character.</value>
|
||||
<data name="TargetElementAttribute_InvalidName" xml:space="preserve">
|
||||
<value>Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character.</value>
|
||||
</data>
|
||||
<data name="TagHelperDescriptorResolver_InvalidTagHelperDirective" xml:space="preserve">
|
||||
<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">
|
||||
<value>Invalid tag helper directive '{0}' value. '{1} is not allowed in prefix '{2}'.</value>
|
||||
</data>
|
||||
<data name="TagHelperDescriptorFactory_Attribute" xml:space="preserve">
|
||||
<value>Attribute</value>
|
||||
</data>
|
||||
<data name="TagHelperDescriptorFactory_Tag" xml:space="preserve">
|
||||
<value>Tag</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,10 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNet.Razor.Parser;
|
||||
using Microsoft.AspNet.Razor.TagHelpers;
|
||||
using Microsoft.AspNet.Razor.Text;
|
||||
using Microsoft.Framework.Internal;
|
||||
|
||||
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:
|
||||
// https://github.com/aspnet/Razor/issues/165
|
||||
|
||||
public static ICollection<char> InvalidNonWhitespaceNameCharacters { get; } = new HashSet<char>(
|
||||
new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'' });
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="TagHelperDescriptor"/> from the given <paramref name="type"/>.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <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 typeName = type.FullName;
|
||||
var typeInfo = type.GetTypeInfo();
|
||||
var attributeDescriptors = GetAttributeDescriptors(type);
|
||||
var targetElementAttributes = GetValidTargetElementAttributes(typeInfo, errorSink);
|
||||
var tagHelperDescriptors =
|
||||
BuildTagHelperDescriptors(
|
||||
typeInfo,
|
||||
assemblyName,
|
||||
attributeDescriptors,
|
||||
targetElementAttributes);
|
||||
|
||||
return tagNames.Select(tagName =>
|
||||
new TagHelperDescriptor(
|
||||
prefix: string.Empty,
|
||||
tagName: tagName,
|
||||
typeName: typeName,
|
||||
assemblyName: assemblyName,
|
||||
attributes: attributeDescriptors));
|
||||
return tagHelperDescriptors.Distinct(TagHelperDescriptorComparer.Default);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetTagNames(Type tagHelperType)
|
||||
private static IEnumerable<TargetElementAttribute> GetValidTargetElementAttributes(
|
||||
TypeInfo typeInfo,
|
||||
ParserErrorSink errorSink)
|
||||
{
|
||||
var typeInfo = tagHelperType.GetTypeInfo();
|
||||
var attributes = typeInfo.GetCustomAttributes<HtmlElementNameAttribute>(inherit: false);
|
||||
var targetElementAttributes = typeInfo.GetCustomAttributes<TargetElementAttribute>(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 (!attributes.Any())
|
||||
if (!targetElementAttributes.Any())
|
||||
{
|
||||
var name = typeInfo.Name;
|
||||
|
||||
|
|
@ -64,11 +85,122 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
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 attributes.SelectMany(attribute => attribute.Tags).Distinct();
|
||||
return targetElementAttributes.Select(
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
{ TagHelperDirectiveType.RemoveTagHelper, SyntaxConstants.CSharp.RemoveTagHelperKeyword },
|
||||
{ TagHelperDirectiveType.TagHelperPrefix, SyntaxConstants.CSharp.TagHelperPrefixKeyword },
|
||||
};
|
||||
private static readonly HashSet<char> InvalidNonWhitespacePrefixCharacters =
|
||||
new HashSet<char>(new[] { '@', '!', '<', '!', '/', '?', '[', '>', ']', '=', '"', '\'' });
|
||||
|
||||
private readonly TagHelperTypeResolver _typeResolver;
|
||||
|
||||
|
|
@ -131,7 +129,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
|
||||
// Convert types to TagHelperDescriptors
|
||||
var descriptors = tagHelperTypes.SelectMany(
|
||||
type => TagHelperDescriptorFactory.CreateDescriptors(assemblyName, type));
|
||||
type => TagHelperDescriptorFactory.CreateDescriptors(assemblyName, type, errorSink));
|
||||
|
||||
return descriptors;
|
||||
}
|
||||
|
|
@ -150,7 +148,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
descriptor.TagName,
|
||||
descriptor.TypeName,
|
||||
descriptor.AssemblyName,
|
||||
descriptor.Attributes));
|
||||
descriptor.Attributes,
|
||||
descriptor.RequiredAttributes));
|
||||
}
|
||||
|
||||
return descriptors;
|
||||
|
|
@ -198,7 +197,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
|||
{
|
||||
// Prefixes are correlated with tag names, tag names cannot have whitespace.
|
||||
if (char.IsWhiteSpace(character) ||
|
||||
InvalidNonWhitespacePrefixCharacters.Contains(character))
|
||||
TagHelperDescriptorFactory.InvalidNonWhitespaceNameCharacters.Contains(character))
|
||||
{
|
||||
errorSink.OnError(
|
||||
directiveLocation,
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -224,9 +224,16 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
|
||||
if (name == null)
|
||||
{
|
||||
errorSink.OnError(span.Start,
|
||||
RazorResources.TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed,
|
||||
span.Content.Length);
|
||||
// We couldn't find a name, if the original span content was whitespace it ultimately means the tag
|
||||
// that owns this "attribute" is malformed and is expecting a user to type a new attribute.
|
||||
// ex: <myTH class="btn"| |
|
||||
if (!string.IsNullOrWhiteSpace(span.Content))
|
||||
{
|
||||
errorSink.OnError(
|
||||
span.Start,
|
||||
RazorResources.TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed,
|
||||
span.Content.Length);
|
||||
}
|
||||
|
||||
attribute = default(KeyValuePair<string, SyntaxTreeNode>);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
public class TagHelperParseTreeRewriter : ISyntaxTreeRewriter
|
||||
{
|
||||
private TagHelperDescriptorProvider _provider;
|
||||
private Stack<TagHelperBlockBuilder> _tagStack;
|
||||
private Stack<TagHelperBlockTracker> _trackerStack;
|
||||
private Stack<BlockBuilder> _blockStack;
|
||||
private BlockBuilder _currentBlock;
|
||||
|
||||
public TagHelperParseTreeRewriter(TagHelperDescriptorProvider provider)
|
||||
{
|
||||
_provider = provider;
|
||||
_tagStack = new Stack<TagHelperBlockBuilder>();
|
||||
_trackerStack = new Stack<TagHelperBlockTracker>();
|
||||
_blockStack = new Stack<BlockBuilder>();
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
CodeGenerator = input.CodeGenerator
|
||||
});
|
||||
|
||||
var activeTagHelpers = _tagStack.Count;
|
||||
var activeTagHelpers = _trackerStack.Count;
|
||||
|
||||
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
|
||||
// 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
|
||||
// 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.
|
||||
BuildMalformedTagHelpers(_tagStack.Count - activeTagHelpers, context);
|
||||
BuildMalformedTagHelpers(_trackerStack.Count - activeTagHelpers, context);
|
||||
|
||||
Debug.Assert(activeTagHelpers == _tagStack.Count);
|
||||
Debug.Assert(activeTagHelpers == _trackerStack.Count);
|
||||
}
|
||||
|
||||
BuildCurrentlyTrackedBlock();
|
||||
|
|
@ -91,8 +91,6 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
|
||||
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)
|
||||
var tagName = GetTagName(tagBlock);
|
||||
|
||||
|
|
@ -104,22 +102,39 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
|
||||
var descriptors = Enumerable.Empty<TagHelperDescriptor>();
|
||||
|
||||
if (IsPotentialTagHelper(tagName, tagBlock))
|
||||
{
|
||||
descriptors = _provider.GetTagHelpers(tagName);
|
||||
}
|
||||
|
||||
// If there aren't any TagHelperDescriptors registered then we aren't a TagHelper
|
||||
if (!descriptors.Any())
|
||||
if (!IsPotentialTagHelper(tagName, tagBlock))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var tracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null;
|
||||
var tagNameScope = tracker?.Builder.TagName ?? string.Empty;
|
||||
|
||||
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(
|
||||
tagName,
|
||||
|
|
@ -144,32 +159,43 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
}
|
||||
else
|
||||
{
|
||||
// We're in an end tag helper block.
|
||||
|
||||
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.
|
||||
// Validate that our end tag matches the currently scoped tag, if not we may need to error.
|
||||
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);
|
||||
}
|
||||
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
|
||||
// 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))
|
||||
{
|
||||
ValidTagStructure(tagName, tagBlock, context);
|
||||
ValidateTagStructure(tagName, tagBlock, context);
|
||||
|
||||
// Successfully recovered, move onto the next element.
|
||||
}
|
||||
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.
|
||||
context.ErrorSink.OnError(
|
||||
tagBlock.Start,
|
||||
|
|
@ -183,13 +209,61 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
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.
|
||||
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 '<'.
|
||||
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 (tagEnd != null && tagEnd.Kind == SpanKind.Markup)
|
||||
|
|
@ -198,18 +272,11 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
|
||||
if (endSymbol != null && endSymbol.Type == HtmlSymbolType.CloseAngle)
|
||||
{
|
||||
invalidStructure = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidStructure)
|
||||
{
|
||||
context.ErrorSink.OnError(
|
||||
tag.Start,
|
||||
RazorResources.FormatTagHelpersParseTreeRewriter_MissingCloseAngle(tagName));
|
||||
}
|
||||
|
||||
return !invalidStructure;
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
// for formatting.
|
||||
_tagStack.Pop().SourceEndTag = endTag;
|
||||
_trackerStack.Pop().Builder.SourceEndTag = endTag;
|
||||
|
||||
BuildCurrentlyTrackedBlock();
|
||||
}
|
||||
|
|
@ -256,11 +323,6 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
childSpan.Kind != SpanKind.Transition;
|
||||
}
|
||||
|
||||
private bool IsRegisteredTagHelper(string tagName)
|
||||
{
|
||||
return _provider.GetTagHelpers(tagName).Any();
|
||||
}
|
||||
|
||||
private void TrackBlock(BlockBuilder builder)
|
||||
{
|
||||
_currentBlock = builder;
|
||||
|
|
@ -270,7 +332,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
|
||||
private void TrackTagHelperBlock(TagHelperBlockBuilder builder)
|
||||
{
|
||||
_tagStack.Push(builder);
|
||||
_trackerStack.Push(new TagHelperBlockTracker(builder));
|
||||
|
||||
TrackBlock(builder);
|
||||
}
|
||||
|
|
@ -279,9 +341,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
{
|
||||
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;
|
||||
}
|
||||
|
|
@ -289,9 +351,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
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.
|
||||
if (malformedTagHelperCount != _tagStack.Count)
|
||||
if (malformedTagHelperCount != _trackerStack.Count)
|
||||
{
|
||||
BuildMalformedTagHelpers(malformedTagHelperCount, context);
|
||||
|
||||
|
|
@ -302,7 +364,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -310,7 +372,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var malformedTagHelper = _tagStack.Peek();
|
||||
var malformedTagHelper = _trackerStack.Peek().Builder;
|
||||
|
||||
context.ErrorSink.OnError(
|
||||
malformedTagHelper.Start,
|
||||
|
|
@ -357,5 +419,17 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
|
|||
Debug.Assert(tagBlock.Type == BlockType.Tag);
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,9 +14,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
/// <summary>
|
||||
/// Internal for testing.
|
||||
/// </summary>
|
||||
internal TagHelperDescriptor([NotNull] string tagName,
|
||||
[NotNull] string typeName,
|
||||
[NotNull] string assemblyName)
|
||||
internal TagHelperDescriptor(
|
||||
[NotNull] string tagName,
|
||||
[NotNull] string typeName,
|
||||
[NotNull] string assemblyName)
|
||||
: this(
|
||||
tagName,
|
||||
typeName,
|
||||
|
|
@ -33,12 +34,31 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
[NotNull] string typeName,
|
||||
[NotNull] string assemblyName,
|
||||
[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(
|
||||
prefix: string.Empty,
|
||||
tagName: tagName,
|
||||
typeName: typeName,
|
||||
assemblyName: assemblyName,
|
||||
attributes: attributes)
|
||||
tagName: tagName,
|
||||
typeName: typeName,
|
||||
assemblyName: assemblyName,
|
||||
attributes: attributes,
|
||||
requiredAttributes: requiredAttributes)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -57,12 +77,16 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
/// <param name="attributes">
|
||||
/// The <see cref="TagHelperAttributeDescriptor"/>s to request from the HTML tag.
|
||||
/// </param>
|
||||
/// <param name="requiredAttributes">
|
||||
/// The attribute names required for the tag helper to target the HTML tag.
|
||||
/// </param>
|
||||
public TagHelperDescriptor(
|
||||
string prefix,
|
||||
[NotNull] string tagName,
|
||||
[NotNull] string typeName,
|
||||
[NotNull] string assemblyName,
|
||||
[NotNull] IEnumerable<TagHelperAttributeDescriptor> attributes)
|
||||
[NotNull] IEnumerable<TagHelperAttributeDescriptor> attributes,
|
||||
[NotNull] IEnumerable<string> requiredAttributes)
|
||||
{
|
||||
Prefix = prefix ?? string.Empty;
|
||||
TagName = tagName;
|
||||
|
|
@ -70,6 +94,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
TypeName = typeName;
|
||||
AssemblyName = assemblyName;
|
||||
Attributes = new List<TagHelperAttributeDescriptor>(attributes);
|
||||
RequiredAttributes = new List<string>(requiredAttributes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -102,6 +127,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
/// <summary>
|
||||
/// The list of attributes the tag helper expects.
|
||||
/// </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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Internal.Web.Utils;
|
||||
|
||||
namespace Microsoft.AspNet.Razor.TagHelpers
|
||||
|
|
@ -29,15 +30,22 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
/// <c>false</c> otherwise.</returns>
|
||||
/// <remarks>
|
||||
/// Determines equality based on <see cref="TagHelperDescriptor.TypeName"/>,
|
||||
/// <see cref="TagHelperDescriptor.AssemblyName"/>, <see cref="TagHelperDescriptor.TagName"/> and
|
||||
/// <see cref="TagHelperDescriptor.Prefix"/>.
|
||||
/// <see cref="TagHelperDescriptor.AssemblyName"/>, <see cref="TagHelperDescriptor.TagName"/>,
|
||||
/// and <see cref="TagHelperDescriptor.RequiredAttributes"/>.
|
||||
/// </remarks>
|
||||
public bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY)
|
||||
{
|
||||
return string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal) &&
|
||||
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>
|
||||
|
|
@ -47,11 +55,22 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
/// <returns>An <see cref="int"/> that uniquely identifies the given <paramref name="descriptor"/>.</returns>
|
||||
public int GetHashCode(TagHelperDescriptor descriptor)
|
||||
{
|
||||
return HashCodeCombiner.Start()
|
||||
.Add(descriptor.TagName, StringComparer.OrdinalIgnoreCase)
|
||||
.Add(descriptor.TypeName, StringComparer.Ordinal)
|
||||
.Add(descriptor.AssemblyName, StringComparer.Ordinal)
|
||||
.CombinedHash;
|
||||
var hashCodeCombiner = HashCodeCombiner
|
||||
.Start()
|
||||
.Add(descriptor.TypeName, StringComparer.Ordinal)
|
||||
.Add(descriptor.TagName, StringComparer.OrdinalIgnoreCase)
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
/// </summary>
|
||||
public class TagHelperDescriptorProvider
|
||||
{
|
||||
private const string CatchAllDescriptorTarget = "*";
|
||||
public const string CatchAllDescriptorTarget = "*";
|
||||
|
||||
private IDictionary<string, HashSet<TagHelperDescriptor>> _registrations;
|
||||
private string _tagHelperPrefix;
|
||||
|
|
@ -37,13 +37,12 @@ namespace Microsoft.AspNet.Razor.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>
|
||||
/// <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> GetTagHelpers(string tagName)
|
||||
public IEnumerable<TagHelperDescriptor> GetDescriptors(string tagName, IEnumerable<string> attributeNames)
|
||||
{
|
||||
HashSet<TagHelperDescriptor> descriptors;
|
||||
|
||||
if (!string.IsNullOrEmpty(_tagHelperPrefix) &&
|
||||
(tagName.Length <= _tagHelperPrefix.Length ||
|
||||
!tagName.StartsWith(_tagHelperPrefix, StringComparison.OrdinalIgnoreCase)))
|
||||
|
|
@ -52,29 +51,54 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
|||
return Enumerable.Empty<TagHelperDescriptor>();
|
||||
}
|
||||
|
||||
HashSet<TagHelperDescriptor> catchAllDescriptors;
|
||||
IEnumerable<TagHelperDescriptor> descriptors;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// If the requested tag name is the catch-all target, we should short circuit.
|
||||
if (tagName.Equals(CatchAllDescriptorTarget, StringComparison.OrdinalIgnoreCase))
|
||||
else
|
||||
{
|
||||
return descriptors;
|
||||
descriptors = catchAllDescriptors;
|
||||
}
|
||||
|
||||
// If we have a tag name associated with the requested name, return the descriptors +
|
||||
// all of the catch-all descriptors.
|
||||
HashSet<TagHelperDescriptor> matchingDescriptors;
|
||||
if (_registrations.TryGetValue(tagName, out matchingDescriptors))
|
||||
// If the requested tag name is the catch-all target, we shouldn't do the work of concatenating extra
|
||||
// descriptors.
|
||||
if (!tagName.Equals(CatchAllDescriptorTarget, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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
|
||||
// of the "catch-all" tag descriptors (there may not be any).
|
||||
return descriptors;
|
||||
var applicableDescriptors = ApplyRequiredAttributes(descriptors, attributeNames);
|
||||
|
||||
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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue