diff --git a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs index e9d4cc8127..f701d0a217 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs @@ -78,6 +78,22 @@ namespace Microsoft.AspNet.Razor.Runtime return string.Format(CultureInfo.CurrentCulture, GetString("ScopeManager_EndCannotBeCalledWithoutACallToBegin"), p0, p1, p2); } + /// + /// Parameter {0} must not contain null tag names. + /// + internal static string TagNameAttribute_AdditionalTagsCannotContainNull + { + get { return GetString("TagNameAttribute_AdditionalTagsCannotContainNull"); } + } + + /// + /// Parameter {0} must not contain null tag names. + /// + internal static string FormatTagNameAttribute_AdditionalTagsCannotContainNull(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagNameAttribute_AdditionalTagsCannotContainNull"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx index 89259d17d4..8e0b4485bb 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx +++ b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx @@ -131,4 +131,7 @@ Must call '{2}.{1}' before calling '{2}.{0}'. + + Parameter {0} must not contain null tag names. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs index fd1b02343c..6113eed120 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs @@ -24,30 +24,40 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// /// The type to create a from. /// A that describes the given . - public static TagHelperDescriptor CreateDescriptor(Type type) + public static IEnumerable CreateDescriptors(Type type) { - var tagName = GetTagName(type); + var tagNames = GetTagNames(type); var typeName = type.FullName; var attributeDescriptors = GetAttributeDescriptors(type); var contentBehavior = GetContentBehavior(type); - return new TagHelperDescriptor(tagName, - typeName, - contentBehavior, - attributeDescriptors); + return tagNames.Select(tagName => + new TagHelperDescriptor(tagName, + typeName, + contentBehavior, + attributeDescriptors)); } - // TODO: Make this method support TagNameAttribute tag names: https://github.com/aspnet/Razor/issues/120 - private static string GetTagName(Type tagHelperType) + private static IEnumerable GetTagNames(Type tagHelperType) { - var name = tagHelperType.Name; + var typeInfo = tagHelperType.GetTypeInfo(); + var attributes = typeInfo.GetCustomAttributes(inherit: false); - if (name.EndsWith(TagHelperNameEnding, StringComparison.OrdinalIgnoreCase)) + // If there isn't an attribute specifying the tag name derive it from the name + if (!attributes.Any()) { - name = name.Substring(0, name.Length - TagHelperNameEnding.Length); + var name = typeInfo.Name; + + if (name.EndsWith(TagHelperNameEnding, StringComparison.OrdinalIgnoreCase)) + { + name = name.Substring(0, name.Length - TagHelperNameEnding.Length); + } + + return new[] { name }; } - return name; + // Remove duplicate tag names. + return attributes.SelectMany(attribute => attribute.Tags).Distinct(); } private static IEnumerable GetAttributeDescriptors(Type type) diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs index 3cd8a5c2c9..fd9cff000b 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs @@ -48,7 +48,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var tagHelperTypes = ResolveTagHelperTypes(lookupStrings); // Convert types to TagHelperDescriptors - var descriptors = tagHelperTypes.Select(TagHelperDescriptorFactory.CreateDescriptor); + var descriptors = tagHelperTypes.SelectMany(TagHelperDescriptorFactory.CreateDescriptors); return descriptors; } diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagNameAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagNameAttribute.cs new file mode 100644 index 0000000000..91d06d193f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagNameAttribute.cs @@ -0,0 +1,50 @@ +// 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; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Used to override a 's default tag name target. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] + public sealed class TagNameAttribute : Attribute + { + /// + /// Instantiates a new instance of the class. + /// + /// The HTML tag name for the to target. + public TagNameAttribute([NotNull] string tag) + { + Tags = new[] { tag }; + } + + /// + /// Instantiates a new instance of the class. + /// + /// The HTML tag name for the to target. + /// Additional HTML tag names for the to target. + public TagNameAttribute([NotNull] string tag, [NotNull] params string[] additionalTags) + { + if (additionalTags.Contains(null)) + { + throw new ArgumentNullException( + nameof(additionalTags), + Resources.FormatTagNameAttribute_AdditionalTagsCannotContainNull(nameof(additionalTags))); + }; + + var allTags = new List(additionalTags); + allTags.Add(tag); + + Tags = allTags; + } + + /// + /// An of tag names for the to target. + /// + public IEnumerable Tags { get; private set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs index 6fd0813131..8aee286fd6 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs @@ -15,9 +15,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var expectedDescriptor = new TagHelperDescriptor("Object", "System.Object", ContentBehavior.None); // Act - var descriptor = TagHelperDescriptorFactory.CreateDescriptor(typeof(object)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(typeof(object)); // Assert + var descriptor = Assert.Single(descriptors); Assert.Equal(descriptor, expectedDescriptor, CompleteTagHelperDescriptorComparer.Default); } @@ -35,9 +36,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }); // Act - var descriptor = TagHelperDescriptorFactory.CreateDescriptor(typeof(SingleAttributeTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(typeof(SingleAttributeTagHelper)); // Assert + var descriptor = Assert.Single(descriptors); Assert.Equal(descriptor, expectedDescriptor, CompleteTagHelperDescriptorComparer.Default); } @@ -56,9 +58,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }); // Act - var descriptor = TagHelperDescriptorFactory.CreateDescriptor(typeof(MissingAccessorTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(typeof(MissingAccessorTagHelper)); // Assert + var descriptor = Assert.Single(descriptors); Assert.Equal(descriptor, expectedDescriptor, CompleteTagHelperDescriptorComparer.Default); } @@ -78,13 +81,13 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }); // Act - var descriptor = TagHelperDescriptorFactory.CreateDescriptor(typeof(PrivateAccessorTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(typeof(PrivateAccessorTagHelper)); // Assert + var descriptor = Assert.Single(descriptors); Assert.Equal(descriptor, expectedDescriptor, CompleteTagHelperDescriptorComparer.Default); } - [Fact] public void CreateDescriptor_ResolvesCustomContentBehavior() { @@ -95,9 +98,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers ContentBehavior.Append); // Act - var descriptor = TagHelperDescriptorFactory.CreateDescriptor(typeof(CustomContentBehaviorTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(typeof(CustomContentBehaviorTagHelper)); // Assert + var descriptor = Assert.Single(descriptors); Assert.Equal(descriptor, expectedDescriptor, CompleteTagHelperDescriptorComparer.Default); } @@ -111,13 +115,114 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers ContentBehavior.None); // Act - var descriptor = TagHelperDescriptorFactory.CreateDescriptor( + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( typeof(InheritedCustomContentBehaviorTagHelper)); // Assert + var descriptor = Assert.Single(descriptors); Assert.Equal(descriptor, expectedDescriptor, CompleteTagHelperDescriptorComparer.Default); } + [Fact] + public void CreateDescriptor_ResolvesMultipleTagHelperDescriptorsFromSingleType() + { + // Arrange + var validProp = typeof(MultiTagTagHelper).GetProperty(nameof(MultiTagTagHelper.ValidAttribute)); + var expectedDescriptors = new[] { + new TagHelperDescriptor( + "div", + typeof(MultiTagTagHelper).FullName, + ContentBehavior.None, + new[] { + new TagHelperAttributeDescriptor(nameof(MultiTagTagHelper.ValidAttribute), validProp) + }), + new TagHelperDescriptor( + "p", + typeof(MultiTagTagHelper).FullName, + ContentBehavior.None, + new[] { + new TagHelperAttributeDescriptor(nameof(MultiTagTagHelper.ValidAttribute), validProp) + }) + }; + + // Act + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(typeof(MultiTagTagHelper)); + + // Assert + Assert.Equal(descriptors, expectedDescriptors, CompleteTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptor_DoesntResolveInheritedTagNames() + { + // Arrange + var validProp = typeof(InheritedMultiTagTagHelper).GetProperty(nameof(InheritedMultiTagTagHelper.ValidAttribute)); + var expectedDescriptor = new TagHelperDescriptor( + "InheritedMultiTag", + typeof(InheritedMultiTagTagHelper).FullName, + ContentBehavior.None, + new[] { + new TagHelperAttributeDescriptor(nameof(InheritedMultiTagTagHelper.ValidAttribute), validProp) + }); + + // Act + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(typeof(InheritedMultiTagTagHelper)); + + // Assert + var descriptor = Assert.Single(descriptors); + Assert.Equal(descriptor, expectedDescriptor, CompleteTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptor_IgnoresDuplicateTagNamesFromAttribute() + { + // Arrange + var expectedDescriptors = new[] { + new TagHelperDescriptor("p", typeof(DuplicateTagNameTagHelper).FullName, ContentBehavior.None), + new TagHelperDescriptor("div", typeof(DuplicateTagNameTagHelper).FullName, ContentBehavior.None) + }; + + // Act + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(typeof(DuplicateTagNameTagHelper)); + + // Assert + Assert.Equal(descriptors, expectedDescriptors, CompleteTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptor_OverridesTagNameFromAttribute() + { + // Arrange + var expectedDescriptors = new[] { + new TagHelperDescriptor("data-condition", + typeof(OverrideNameTagHelper).FullName, + ContentBehavior.None), + }; + + // Act + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(typeof(OverrideNameTagHelper)); + + // Assert + Assert.Equal(descriptors, expectedDescriptors, CompleteTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptor_GetsTagNamesFromMultipleAttributes() + { + // Arrange + var expectedDescriptors = new[] { + new TagHelperDescriptor("span", typeof(MultipleAttributeTagHelper).FullName, ContentBehavior.None), + new TagHelperDescriptor("p", typeof(MultipleAttributeTagHelper).FullName, ContentBehavior.None), + new TagHelperDescriptor("div", typeof(MultipleAttributeTagHelper).FullName, ContentBehavior.None) + }; + + // Act + var descriptors = TagHelperDescriptorFactory.CreateDescriptors(typeof(MultipleAttributeTagHelper)); + + // Assert + Assert.Equal(descriptors, expectedDescriptors, CompleteTagHelperDescriptorComparer.Default); + } + [ContentBehavior(ContentBehavior.Append)] private class CustomContentBehaviorTagHelper { @@ -126,5 +231,31 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers private class InheritedCustomContentBehaviorTagHelper : CustomContentBehaviorTagHelper { } + + [TagName("p", "div")] + private class MultiTagTagHelper + { + public string ValidAttribute { get; set; } + } + + private class InheritedMultiTagTagHelper : MultiTagTagHelper + { + } + + [TagName("p", "p", "div", "div")] + private class DuplicateTagNameTagHelper + { + } + + [TagName("data-condition")] + private class OverrideNameTagHelper + { + } + + [TagName("span")] + [TagName("div", "p")] + private class MultipleAttributeTagHelper + { + } } } \ No newline at end of file