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>
|
/// <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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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.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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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>);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue