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

Commit

Permalink
Add TagHelper attribute targeting.
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
NTaylorMullen committed Mar 5, 2015
1 parent 4bd02ba commit 7a89ce2
Show file tree
Hide file tree
Showing 11 changed files with 402 additions and 176 deletions.
24 changes: 12 additions & 12 deletions src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions src/Microsoft.AspNet.Razor.Runtime/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,17 @@
<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>
</data>
<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>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
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;

namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
Expand All @@ -15,6 +17,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// </summary>
public static class TagHelperDescriptorFactory
{
public static readonly ISet<char> InvalidNonWhitespaceNameCharacters = new HashSet<char>(
new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'' });

private const string TagHelperNameEnding = "TagHelper";
private const string HtmlCaseRegexReplacement = "-$1$2";

Expand All @@ -34,26 +39,32 @@ public static class TagHelperDescriptorFactory
/// <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,
Type type,
ParserErrorSink errorSink)
{
var tagNames = GetTagNames(type);
var elementTargets = GetElementTargets(type, errorSink);
var typeName = type.FullName;
var attributeDescriptors = GetAttributeDescriptors(type);

return tagNames.Select(tagName =>
new TagHelperDescriptor(tagName,
typeName,
assemblyName,
attributeDescriptors));
return elementTargets.Select(
elementTarget =>
new TagHelperDescriptor(
elementTarget.TagName,
typeName,
assemblyName,
attributeDescriptors,
elementTarget.AttributeNames));
}

private static IEnumerable<string> GetTagNames(Type tagHelperType)
private static IEnumerable<ElementTarget> GetElementTargets(Type tagHelperType, ParserErrorSink errorSink)
{
var typeInfo = tagHelperType.GetTypeInfo();
var attributes = typeInfo.GetCustomAttributes<HtmlElementNameAttribute>(inherit: false);
var targetElementAttributes = typeInfo.GetCustomAttributes<TargetElementAttribute>(inherit: false);

// 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;

Expand All @@ -62,11 +73,105 @@ private static IEnumerable<string> GetTagNames(Type tagHelperType)
name = name.Substring(0, name.Length - TagHelperNameEnding.Length);
}

return new[] { ToHtmlCase(name) };
return new[]
{
new ElementTarget
{
TagName = ToHtmlCase(name),
AttributeNames = Enumerable.Empty<string>()
}
};
}

// Remove duplicate tag names.
return attributes.SelectMany(attribute => attribute.Tags).Distinct();
return targetElementAttributes.SelectMany(
targetElementAttribute =>
{
IEnumerable<string> tagNames;
IEnumerable<string> attributeNames = null;
if (!TryGetValidatedNames(
targetElementAttribute.Tags,
pascalNameTarget: "Tag",
errorSink: errorSink,
names: out tagNames) ||
(targetElementAttribute.Attributes != null &&
!TryGetValidatedNames(
targetElementAttribute.Attributes,
pascalNameTarget: "Attribute",
errorSink: errorSink,
names: out attributeNames)))
{
return Enumerable.Empty<ElementTarget>();
}
return BuildElementTargets(tagNames, attributeNames, tagHelperType);
}).ToArray();
}

// Internal for testing
internal static bool TryGetValidatedNames(
string commaSeparatedNames,
string pascalNameTarget,
ParserErrorSink errorSink,
out IEnumerable<string> names)
{
names = GetCommaSeparatedValues(commaSeparatedNames)?.Distinct();

return ValidateNames(names, pascalNameTarget, errorSink);
}

private 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());
}

private static bool ValidateNames(
IEnumerable<string> names,
string pascalNameTarget,
ParserErrorSink errorSink)
{
foreach (var name in names)
{
if (string.IsNullOrWhiteSpace(name))
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTargetElementAttribute_NameCannotBeNullOrWhitespace(pascalNameTarget));

return false;
}

foreach (var character in name)
{
if (InvalidNonWhitespaceNameCharacters.Contains(character))
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTargetElementAttribute_InvalidName(
pascalNameTarget.ToLowerInvariant(),
name,
character));

return false;
}
}
}

return true;
}

private static IEnumerable<ElementTarget> BuildElementTargets(
IEnumerable<string> tagNames,
IEnumerable<string> attributeNames,
Type tagHelperType)
{
return tagNames.Select(tagName =>
new ElementTarget
{
TagName = tagName,
AttributeNames = attributeNames ?? Enumerable.Empty<string>()
});
}

private static IEnumerable<TagHelperAttributeDescriptor> GetAttributeDescriptors(Type type)
Expand Down Expand Up @@ -111,5 +216,11 @@ private static string ToHtmlCase(string name)
{
return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant();
}

private class ElementTarget
{
public string TagName { get; set; }
public IEnumerable<string> AttributeNames { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ public class TagHelperDescriptorResolver : ITagHelperDescriptorResolver
{ TagHelperDirectiveType.RemoveTagHelper, SyntaxConstants.CSharp.RemoveTagHelperKeyword },
{ TagHelperDirectiveType.TagHelperPrefix, SyntaxConstants.CSharp.TagHelperPrefixKeyword },
};
private static readonly HashSet<char> InvalidNonWhitespacePrefixCharacters =
new HashSet<char>(new[] { '@', '!', '<', '!', '/', '?', '[', '>', ']', '=', '"', '\'' });

private readonly TagHelperTypeResolver _typeResolver;

Expand Down Expand Up @@ -131,7 +129,7 @@ protected virtual IEnumerable<TagHelperDescriptor> ResolveDescriptorsInAssembly(

// Convert types to TagHelperDescriptors
var descriptors = tagHelperTypes.SelectMany(
type => TagHelperDescriptorFactory.CreateDescriptors(assemblyName, type));
type => TagHelperDescriptorFactory.CreateDescriptors(assemblyName, type, errorSink));

return descriptors;
}
Expand All @@ -150,7 +148,8 @@ private static IEnumerable<TagHelperDescriptor> PrefixDescriptors(
descriptor.TagName,
descriptor.TypeName,
descriptor.AssemblyName,
descriptor.Attributes));
descriptor.Attributes,
descriptor.RequiredAttributes));
}

return descriptors;
Expand Down Expand Up @@ -198,7 +197,7 @@ private static bool EnsureValidPrefix(
{
// 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,
Expand Down
Loading

0 comments on commit 7a89ce2

Please sign in to comment.