Add `ParentTag` to `[HtmlTargetElement]`.

- `ParentTag` allows `TagHelper`s to restrict where they apply based on their immediate parent tag.
- Changed the `TagHelperParseTreeRewriter` to understand non-`TagHelper` HTML elements to properly determine a parent tag when applying `TagHelperDescriptor.RequiredParent`. This change will also enable `[RestrictChildren]` to apply to more than just `TagHelper`s.
- Added tests to validate that partial and well formed tags properly discover `TagHelper`s. Also added tests that validate that descriptors are properly created based on `TagHelper` types.

#474
This commit is contained in:
N. Taylor Mullen 2015-09-21 17:51:56 -07:00
parent 18799d2944
commit 67739ea565
15 changed files with 812 additions and 37 deletions

View File

@ -426,6 +426,22 @@ namespace Microsoft.AspNet.Razor.Runtime
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace"), p0, p1);
}
/// <summary>
/// Parent Tag
/// </summary>
internal static string TagHelperDescriptorFactory_ParentTag
{
get { return GetString("TagHelperDescriptorFactory_ParentTag"); }
}
/// <summary>
/// Parent Tag
/// </summary>
internal static string FormatTagHelperDescriptorFactory_ParentTag()
{
return GetString("TagHelperDescriptorFactory_ParentTag");
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -195,4 +195,7 @@
<data name="TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace" xml:space="preserve">
<value>Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace.</value>
</data>
<data name="TagHelperDescriptorFactory_ParentTag" xml:space="preserve">
<value>Parent Tag</value>
</data>
</root>

View File

@ -79,5 +79,11 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// </para>
/// </remarks>
public TagStructure TagStructure { get; set; }
/// <summary>
/// The required HTML element name of the direct parent.
/// </summary>
/// <remarks>A <c>null</c> value indicates any HTML element name is appropriate.</remarks>
public string ParentTag { get; set; }
}
}

View File

@ -142,6 +142,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: allowedChildren,
tagStructure: default(TagStructure),
parentTag: null,
designTimeDescriptor: typeDesignTimeDescriptor)
};
}
@ -231,6 +232,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
attributeDescriptors,
requiredAttributes,
allowedChildren,
targetElementAttribute.ParentTag,
targetElementAttribute.TagStructure,
designTimeDescriptor);
}
@ -242,6 +244,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,
IEnumerable<string> requiredAttributes,
IEnumerable<string> allowedChildren,
string parentTag,
TagStructure tagStructure,
TagHelperDesignTimeDescriptor designTimeDescriptor)
{
@ -253,6 +256,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
Attributes = attributeDescriptors,
RequiredAttributes = requiredAttributes,
AllowedChildren = allowedChildren,
RequiredParent = parentTag,
TagStructure = tagStructure,
DesignTimeDescriptor = designTimeDescriptor
};
@ -286,7 +290,27 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
}
}
return validTagName && validAttributeNames;
var validParentTagName = ValidateParentTagName(attribute.ParentTag, errorSink);
return validTagName && validAttributeNames && validParentTagName;
}
/// <summary>
/// Internal for unit testing.
/// </summary>
internal static bool ValidateParentTagName(string parentTag, ErrorSink errorSink)
{
return parentTag == null ||
TryValidateName(
parentTag,
Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace(
Resources.TagHelperDescriptorFactory_ParentTag),
characterErrorBuilder: (invalidCharacter) =>
Resources.FormatHtmlTargetElementAttribute_InvalidName(
Resources.TagHelperDescriptorFactory_ParentTag.ToLower(),
parentTag,
invalidCharacter),
errorSink: errorSink);
}
private static bool ValidateName(

View File

@ -161,6 +161,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
Attributes = descriptor.Attributes,
RequiredAttributes = descriptor.RequiredAttributes,
AllowedChildren = descriptor.AllowedChildren,
RequiredParent = descriptor.RequiredParent,
TagStructure = descriptor.TagStructure,
DesignTimeDescriptor = descriptor.DesignTimeDescriptor
});

View File

@ -34,6 +34,7 @@ namespace Microsoft.AspNet.Razor.Test.Internal
Assert.Equal(descriptorX.TagName, descriptorY.TagName, StringComparer.Ordinal);
Assert.Equal(descriptorX.Prefix, descriptorY.Prefix, StringComparer.Ordinal);
Assert.Equal(descriptorX.RequiredAttributes, descriptorY.RequiredAttributes, StringComparer.Ordinal);
Assert.Equal(descriptorX.RequiredParent, descriptorY.RequiredParent, StringComparer.Ordinal);
if (descriptorX.AllowedChildren != descriptorY.AllowedChildren)
{

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
@ -15,16 +14,38 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
{
public class TagHelperParseTreeRewriter : ISyntaxTreeRewriter
{
// From http://dev.w3.org/html5/spec/Overview.html#elements-0
private static readonly HashSet<string> VoidElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"area",
"base",
"br",
"col",
"command",
"embed",
"hr",
"img",
"input",
"keygen",
"link",
"meta",
"param",
"source",
"track",
"wbr"
};
private TagHelperDescriptorProvider _provider;
private Stack<TagHelperBlockTracker> _trackerStack;
private Stack<TagBlockTracker> _trackerStack;
private TagHelperBlockTracker _currentTagHelperTracker;
private Stack<BlockBuilder> _blockStack;
private BlockBuilder _currentBlock;
private string _currentParentTagName;
public TagHelperParseTreeRewriter(TagHelperDescriptorProvider provider)
{
_provider = provider;
_trackerStack = new Stack<TagHelperBlockTracker>();
_trackerStack = new Stack<TagBlockTracker>();
_blockStack = new Stack<BlockBuilder>();
}
@ -44,7 +65,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
ChunkGenerator = input.ChunkGenerator
});
var activeTagHelpers = _trackerStack.Count;
var activeTrackers = _trackerStack.Count;
foreach (var child in input.Children)
{
@ -60,6 +81,8 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
}
else
{
TrackTagBlock(childBlock);
// Non-TagHelper tag.
ValidateParentTagHelperAllowsPlainTag(childBlock, context.ErrorSink);
}
@ -88,19 +111,48 @@ 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 != _trackerStack.Count)
if (activeTrackers != _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(_trackerStack.Count - activeTagHelpers, context);
BuildMalformedTagHelpers(_trackerStack.Count - activeTrackers, context);
Debug.Assert(activeTagHelpers == _trackerStack.Count);
Debug.Assert(activeTrackers == _trackerStack.Count);
}
BuildCurrentlyTrackedBlock();
}
private void TrackTagBlock(Block childBlock)
{
var tagName = GetTagName(childBlock);
// Don't want to track incomplete tags that have no tag name.
if (string.IsNullOrWhiteSpace(tagName))
{
return;
}
if (IsEndTag(childBlock))
{
var parentTracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null;
if (parentTracker != null &&
!parentTracker.IsTagHelper &&
string.Equals(parentTracker.TagName, tagName, StringComparison.OrdinalIgnoreCase))
{
PopTrackerStack();
}
}
else if (!VoidElements.Contains(tagName) && !IsSelfClosing(childBlock))
{
// If it's not a void element and it's not self-closing then we need to create a tag
// tracker for it.
var tracker = new TagBlockTracker(tagName, isTagHelper: false);
PushTrackerStack(tracker);
}
}
private bool TryRewriteTagHelper(Block tagBlock, RewritingContext context)
{
// Get tag name of the current block (doesn't matter if it's an end or start tag)
@ -120,15 +172,14 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
}
var tracker = _currentTagHelperTracker;
var tagNameScope = tracker?.Builder.TagName ?? string.Empty;
var tagNameScope = tracker?.TagName ?? string.Empty;
if (!IsEndTag(tagBlock))
{
// 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);
descriptors = _provider.GetDescriptors(tagName, providedAttributes);
descriptors = _provider.GetDescriptors(tagName, providedAttributes, _currentParentTagName);
// If there aren't any TagHelperDescriptors registered then we aren't a TagHelper
if (!descriptors.Any())
@ -193,7 +244,10 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
}
else
{
descriptors = _provider.GetDescriptors(tagName, attributeNames: Enumerable.Empty<string>());
descriptors = _provider.GetDescriptors(
tagName,
attributeNames: Enumerable.Empty<string>(),
parentTagName: _currentParentTagName);
// 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.
@ -297,7 +351,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
errorSink.OnError(
errorStart,
RazorResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent(
_currentTagHelperTracker.Builder.TagName,
_currentTagHelperTracker.TagName,
allowedChildrenString),
length);
}
@ -343,7 +397,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
var allowedChildrenString = string.Join(", ", tracker.AllowedChildren);
var errorMessage = RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag(
tagName,
tracker.Builder.TagName,
tracker.TagName,
allowedChildrenString);
var errorStart = GetTagDeclarationErrorStart(tagBlock);
@ -450,11 +504,23 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
private void BuildCurrentlyTrackedTagHelperBlock(Block endTag)
{
Debug.Assert(_trackerStack.Any(tracker => tracker.IsTagHelper));
// We need to pop all trackers until we reach our TagHelperBlock. We can throw away any non-TagHelper
// trackers because they don't need to be well-formed.
TagHelperBlockTracker tagHelperTracker;
do
{
tagHelperTracker = PopTrackerStack() as TagHelperBlockTracker;
}
while (tagHelperTracker == null);
// Track the original end tag so the editor knows where each piece of the TagHelperBlock lies
// for formatting.
_trackerStack.Pop().Builder.SourceEndTag = endTag;
tagHelperTracker.Builder.SourceEndTag = endTag;
_currentTagHelperTracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null;
_currentTagHelperTracker =
(TagHelperBlockTracker)_trackerStack.FirstOrDefault(tagBlockTracker => tagBlockTracker.IsTagHelper);
BuildCurrentlyTrackedBlock();
}
@ -481,7 +547,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
private void TrackTagHelperBlock(TagHelperBlockBuilder builder)
{
_currentTagHelperTracker = new TagHelperBlockTracker(builder);
_trackerStack.Push(_currentTagHelperTracker);
PushTrackerStack(_currentTagHelperTracker);
TrackBlock(builder);
}
@ -492,7 +558,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
foreach (var tracker in _trackerStack)
{
if (tracker.Builder.TagName.Equals(tagName, StringComparison.OrdinalIgnoreCase))
if (tracker.IsTagHelper && tracker.TagName.Equals(tagName, StringComparison.OrdinalIgnoreCase))
{
break;
}
@ -521,7 +587,16 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
{
for (var i = 0; i < count; i++)
{
var malformedTagHelper = _trackerStack.Peek().Builder;
var tracker = _trackerStack.Peek();
// Skip all non-TagHelper entries. Non TagHelper trackers do not need to represent well-formed HTML.
if (!tracker.IsTagHelper)
{
PopTrackerStack();
continue;
}
var malformedTagHelper = ((TagHelperBlockTracker)tracker).Builder;
context.ErrorSink.OnError(
SourceLocation.Advance(malformedTagHelper.Start, "<"),
@ -570,11 +645,46 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
Debug.Assert(tagBlock.Children.First() is Span);
}
private class TagHelperBlockTracker
private static bool IsSelfClosing(Block childBlock)
{
var childSpan = childBlock.FindLastDescendentSpan();
return childSpan?.Content.EndsWith("/>") ?? false;
}
private void PushTrackerStack(TagBlockTracker tracker)
{
_currentParentTagName = tracker.TagName;
_trackerStack.Push(tracker);
}
private TagBlockTracker PopTrackerStack()
{
var poppedTracker = _trackerStack.Pop();
_currentParentTagName = _trackerStack.Count > 0 ? _trackerStack.Peek().TagName : null;
return poppedTracker;
}
private class TagBlockTracker
{
public TagBlockTracker(string tagName, bool isTagHelper)
{
TagName = tagName;
IsTagHelper = isTagHelper;
}
public string TagName { get; }
public bool IsTagHelper { get; }
}
private class TagHelperBlockTracker : TagBlockTracker
{
private IEnumerable<string> _prefixedAllowedChildren;
public TagHelperBlockTracker(TagHelperBlockBuilder builder)
: base(builder.TagName, isTagHelper: true)
{
Builder = builder;

View File

@ -163,6 +163,12 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <remarks><c>null</c> indicates all children are allowed.</remarks>
public IEnumerable<string> AllowedChildren { get; set; }
/// <summary>
/// Get the name of the HTML element required as the immediate parent.
/// </summary>
/// <remarks><c>null</c> indicates no restriction on parent tag.</remarks>
public string RequiredParent { get; set; }
/// <summary>
/// The expected tag structure.
/// </summary>

View File

@ -46,6 +46,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal) &&
string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal) &&
string.Equals(
descriptorX.RequiredParent,
descriptorY.RequiredParent,
StringComparison.OrdinalIgnoreCase) &&
Enumerable.SequenceEqual(
descriptorX.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase),
descriptorY.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase),
@ -72,6 +76,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
hashCodeCombiner.Add(descriptor.TypeName, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.TagName, StringComparer.OrdinalIgnoreCase);
hashCodeCombiner.Add(descriptor.AssemblyName, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.RequiredParent, StringComparer.OrdinalIgnoreCase);
hashCodeCombiner.Add(descriptor.TagStructure);
var attributes = descriptor.RequiredAttributes.OrderBy(

View File

@ -40,10 +40,14 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <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>
/// <param name="parentTagName">The parent tag name of the given <paramref name="tagName"/> tag.</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> GetDescriptors(string tagName, IEnumerable<string> attributeNames)
public IEnumerable<TagHelperDescriptor> GetDescriptors(
string tagName,
IEnumerable<string> attributeNames,
string parentTagName)
{
if (!string.IsNullOrEmpty(_tagHelperPrefix) &&
(tagName.Length <= _tagHelperPrefix.Length ||
@ -75,10 +79,20 @@ namespace Microsoft.AspNet.Razor.TagHelpers
}
var applicableDescriptors = ApplyRequiredAttributes(descriptors, attributeNames);
applicableDescriptors = ApplyParentTagFilter(applicableDescriptors, parentTagName);
return applicableDescriptors;
}
private IEnumerable<TagHelperDescriptor> ApplyParentTagFilter(
IEnumerable<TagHelperDescriptor> descriptors,
string parentTagName)
{
return descriptors.Where(descriptor =>
descriptor.RequiredParent == null ||
string.Equals(parentTagName, descriptor.RequiredParent, StringComparison.OrdinalIgnoreCase));
}
private IEnumerable<TagHelperDescriptor> ApplyRequiredAttributes(
IEnumerable<TagHelperDescriptor> descriptors,
IEnumerable<string> attributeNames)

View File

@ -13,11 +13,100 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
public abstract class TagHelperDescriptorFactoryTest
{
protected static readonly AssemblyName TagHelperDescriptorFactoryTestAssembly =
protected static readonly AssemblyName TagHelperDescriptorFactoryTestAssembly =
typeof(TagHelperDescriptorFactoryTest).GetTypeInfo().Assembly.GetName();
protected static readonly string AssemblyName = TagHelperDescriptorFactoryTestAssembly.Name;
public static TheoryData RequiredParentData
{
get
{
// tagHelperType, expectedDescriptors
return new TheoryData<Type, TagHelperDescriptor[]>
{
{
typeof(RequiredParentTagHelper),
new[]
{
new TagHelperDescriptor
{
TagName = "input",
TypeName = typeof(RequiredParentTagHelper).FullName,
AssemblyName = AssemblyName,
RequiredParent = "div"
}
}
},
{
typeof(MultiSpecifiedRequiredParentTagHelper),
new[]
{
new TagHelperDescriptor
{
TagName = "input",
TypeName = typeof(MultiSpecifiedRequiredParentTagHelper).FullName,
AssemblyName = AssemblyName,
RequiredParent = "section"
},
new TagHelperDescriptor
{
TagName = "p",
TypeName = typeof(MultiSpecifiedRequiredParentTagHelper).FullName,
AssemblyName = AssemblyName,
RequiredParent = "div"
}
}
},
{
typeof(MultiWithUnspecifiedRequiredParentTagHelper),
new[]
{
new TagHelperDescriptor
{
TagName = "input",
TypeName = typeof(MultiWithUnspecifiedRequiredParentTagHelper).FullName,
AssemblyName = AssemblyName,
RequiredParent = "div"
},
new TagHelperDescriptor
{
TagName = "p",
TypeName = typeof(MultiWithUnspecifiedRequiredParentTagHelper).FullName,
AssemblyName = AssemblyName
}
}
},
};
}
}
[Theory]
[MemberData(nameof(RequiredParentData))]
public void CreateDescriptors_CreatesDesignTimeDescriptorsWithRequiredParent(
Type tagHelperType,
TagHelperDescriptor[] expectedDescriptors)
{
// Arrange
var errorSink = new ErrorSink();
// Act
var descriptors = TagHelperDescriptorFactory.CreateDescriptors(
AssemblyName,
GetTypeInfo(tagHelperType),
designTime: false,
errorSink: errorSink);
// Assert
Assert.Empty(errorSink.Errors);
// We don't care about order. Mono returns reflected attributes differently so we need to ensure order
// doesn't matter by sorting.
descriptors = descriptors.OrderBy(descriptor => descriptor.TagName);
Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default);
}
public static TheoryData RestrictChildrenData
{
get
@ -1713,6 +1802,41 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
Assert.Equal(expectedErrors, errorSink.Errors);
}
public static TheoryData<string, string[]> InvalidParentTagData
{
get
{
var nullOrWhiteSpaceError =
Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace(
Resources.TagHelperDescriptorFactory_ParentTag);
return GetInvalidNameOrPrefixData(
onNameError: (invalidInput, invalidCharacter) =>
Resources.FormatHtmlTargetElementAttribute_InvalidName(
Resources.TagHelperDescriptorFactory_ParentTag.ToLower(),
invalidInput,
invalidCharacter),
whitespaceErrorString: nullOrWhiteSpaceError,
onDataError: null);
}
}
[Theory]
[MemberData(nameof(InvalidParentTagData))]
public void ValidateParentTagName_AddsExpectedErrors(string name, string[] expectedErrorMessages)
{
// Arrange
var errorSink = new ErrorSink();
var expectedErrors = expectedErrorMessages.Select(
message => new RazorError(message, SourceLocation.Zero, 0));
// Act
TagHelperDescriptorFactory.ValidateParentTagName(name, errorSink);
// Assert
Assert.Equal(expectedErrors, errorSink.Errors);
}
private static TheoryData<string, string[]> GetInvalidNameOrPrefixData(
Func<string, string, string> onNameError,
string whitespaceErrorString,

View File

@ -9,6 +9,23 @@ using Microsoft.AspNet.Razor.Runtime.TagHelpers;
namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
[HtmlTargetElement("input", ParentTag = "div")]
public class RequiredParentTagHelper : TagHelper
{
}
[HtmlTargetElement("p", ParentTag = "div")]
[HtmlTargetElement("input", ParentTag = "section")]
public class MultiSpecifiedRequiredParentTagHelper : TagHelper
{
}
[HtmlTargetElement("p")]
[HtmlTargetElement("input", ParentTag = "div")]
public class MultiWithUnspecifiedRequiredParentTagHelper : TagHelper
{
}
[RestrictChildren("p")]
public class RestrictChildrenTagHelper

View File

@ -4,12 +4,94 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.AspNet.Razor.Test.Internal;
using Xunit;
namespace Microsoft.AspNet.Razor.TagHelpers
{
public class TagHelperDescriptorProviderTest
{
public static TheoryData RequiredParentData
{
get
{
var strongPParent = new TagHelperDescriptor
{
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "p",
};
var strongDivParent = new TagHelperDescriptor
{
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "div",
};
var catchAllPParent = new TagHelperDescriptor
{
TagName = "*",
TypeName = "CatchAllTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "p",
};
return new TheoryData<
string, // tagName
string, // parentTagName
IEnumerable<TagHelperDescriptor>, // availableDescriptors
IEnumerable<TagHelperDescriptor>> // expectedDescriptors
{
{
"strong",
"p",
new[] { strongPParent, strongDivParent },
new[] { strongPParent }
},
{
"strong",
"div",
new[] { strongPParent, strongDivParent, catchAllPParent },
new[] { strongDivParent }
},
{
"strong",
"p",
new[] { strongPParent, strongDivParent, catchAllPParent },
new[] { strongPParent, catchAllPParent }
},
{
"custom",
"p",
new[] { strongPParent, strongDivParent, catchAllPParent },
new[] { catchAllPParent }
},
};
}
}
[Theory]
[MemberData(nameof(RequiredParentData))]
public void GetDescriptors_ReturnsDescriptorsWithRequiredAttributes(
string tagName,
string parentTagName,
IEnumerable<TagHelperDescriptor> availableDescriptors,
IEnumerable<TagHelperDescriptor> expectedDescriptors)
{
// Arrange
var provider = new TagHelperDescriptorProvider(availableDescriptors);
// Act
var resolvedDescriptors = provider.GetDescriptors(
tagName,
attributeNames: Enumerable.Empty<string>(),
parentTagName: parentTagName);
// Assert
Assert.Equal(expectedDescriptors, resolvedDescriptors, CaseSensitiveTagHelperDescriptorComparer.Default);
}
public static TheoryData RequiredAttributeData
{
get
@ -163,10 +245,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var provider = new TagHelperDescriptorProvider(availableDescriptors);
// Act
var resolvedDescriptors = provider.GetDescriptors(tagName, providedAttributes);
var resolvedDescriptors = provider.GetDescriptors(tagName, providedAttributes, parentTagName: "p");
// Assert
Assert.Equal(expectedDescriptors, resolvedDescriptors, TagHelperDescriptorComparer.Default);
Assert.Equal(expectedDescriptors, resolvedDescriptors, CaseSensitiveTagHelperDescriptorComparer.Default);
}
[Fact]
@ -181,7 +263,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var resolvedDescriptors = provider.GetDescriptors("th", attributeNames: Enumerable.Empty<string>());
var resolvedDescriptors = provider.GetDescriptors(
tagName: "th",
attributeNames: Enumerable.Empty<string>(),
parentTagName: "p");
// Assert
Assert.Empty(resolvedDescriptors);
@ -197,8 +282,14 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors("th:div", attributeNames: Enumerable.Empty<string>());
var retrievedDescriptorsSpan = provider.GetDescriptors("th2:span", attributeNames: Enumerable.Empty<string>());
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "th:div",
attributeNames: Enumerable.Empty<string>(),
parentTagName: "p");
var retrievedDescriptorsSpan = provider.GetDescriptors(
tagName: "th2:span",
attributeNames: Enumerable.Empty<string>(),
parentTagName: "p");
// Assert
var descriptor = Assert.Single(retrievedDescriptorsDiv);
@ -215,8 +306,14 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors("th:div", attributeNames: Enumerable.Empty<string>());
var retrievedDescriptorsSpan = provider.GetDescriptors("th:span", attributeNames: Enumerable.Empty<string>());
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "th:div",
attributeNames: Enumerable.Empty<string>(),
parentTagName: "p");
var retrievedDescriptorsSpan = provider.GetDescriptors(
tagName: "th:span",
attributeNames: Enumerable.Empty<string>(),
parentTagName: "p");
// Assert
var descriptor = Assert.Single(retrievedDescriptorsDiv);
@ -234,7 +331,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptors = provider.GetDescriptors("th:div", attributeNames: Enumerable.Empty<string>());
var retrievedDescriptors = provider.GetDescriptors(
tagName: "th:div",
attributeNames: Enumerable.Empty<string>(),
parentTagName: "p");
// Assert
var descriptor = Assert.Single(retrievedDescriptors);
@ -252,7 +352,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors("div", attributeNames: Enumerable.Empty<string>());
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "div",
attributeNames: Enumerable.Empty<string>(),
parentTagName: "p");
// Assert
Assert.Empty(retrievedDescriptorsDiv);
@ -278,7 +381,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptors = provider.GetDescriptors("foo", attributeNames: Enumerable.Empty<string>());
var retrievedDescriptors = provider.GetDescriptors(
tagName: "foo",
attributeNames: Enumerable.Empty<string>(),
parentTagName: "p");
// Assert
Assert.Empty(retrievedDescriptors);
@ -310,8 +416,14 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var divDescriptors = provider.GetDescriptors("div", attributeNames: Enumerable.Empty<string>());
var spanDescriptors = provider.GetDescriptors("span", attributeNames: Enumerable.Empty<string>());
var divDescriptors = provider.GetDescriptors(
tagName: "div",
attributeNames: Enumerable.Empty<string>(),
parentTagName: "p");
var spanDescriptors = provider.GetDescriptors(
tagName: "span",
attributeNames: Enumerable.Empty<string>(),
parentTagName: "p");
// Assert
// For divs
@ -339,7 +451,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptors = provider.GetDescriptors("div", attributeNames: Enumerable.Empty<string>());
var retrievedDescriptors = provider.GetDescriptors(
tagName: "div",
attributeNames: Enumerable.Empty<string>(),
parentTagName: "p");
// Assert
var descriptor = Assert.Single(retrievedDescriptors);

View File

@ -24,12 +24,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
AssemblyName = "assembly name",
RequiredAttributes = new[] { "required attribute one", "required attribute two" },
AllowedChildren = new[] { "allowed child one" },
RequiredParent = "parent name",
DesignTimeDescriptor = new TagHelperDesignTimeDescriptor
{
Summary = "usage summary",
Remarks = "usage remarks",
OutputElementHint = "some-tag"
}
},
};
var expectedSerializedDescriptor =
@ -42,6 +43,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" +
"[\"required attribute one\",\"required attribute two\"]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\"]," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":{{"+
$"\"{ nameof(TagHelperDesignTimeDescriptor.Summary) }\":\"usage summary\"," +
@ -105,6 +107,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":null," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":1," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}";
@ -143,7 +146,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
IsStringProperty = true
},
},
AllowedChildren = new[] { "allowed child one", "allowed child two" }
AllowedChildren = new[] { "allowed child one", "allowed child two" },
RequiredParent = "parent name"
};
var expectedSerializedDescriptor =
@ -167,6 +171,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}";
@ -191,6 +196,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{nameof(TagHelperDescriptor.RequiredAttributes)}\":" +
"[\"required attribute one\",\"required attribute two\"]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," +
$"\"{nameof(TagHelperDescriptor.TagStructure)}\":2," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":{{" +
$"\"{ nameof(TagHelperDesignTimeDescriptor.Summary) }\":\"usage summary\"," +
@ -204,6 +210,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
AssemblyName = "assembly name",
RequiredAttributes = new[] { "required attribute one", "required attribute two" },
AllowedChildren = new[] { "allowed child one", "allowed child two" },
RequiredParent = "parent name",
DesignTimeDescriptor = new TagHelperDesignTimeDescriptor
{
Summary = "usage summary",
@ -255,6 +262,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":null," +
$"\"{nameof(TagHelperDescriptor.TagStructure)}\":0," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}";
var expectedDescriptor = new TagHelperDescriptor
@ -321,6 +329,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":null," +
$"\"{nameof(TagHelperDescriptor.TagStructure)}\":1," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}";
var expectedDescriptor = new TagHelperDescriptor

View File

@ -18,6 +18,330 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
{
public class TagHelperParseTreeRewriterTest : TagHelperRewritingTestBase
{
public static TheoryData PartialRequiredParentData
{
get
{
var factory = CreateDefaultSpanFactory();
var blockFactory = new BlockFactory(factory);
Func<int, string, RazorError> errorFormatUnclosed = (location, tagName) =>
new RazorError(
$"Found a malformed '{tagName}' tag helper. Tag helpers must have a start and end tag or be " +
"self closing.",
new SourceLocation(location, 0, location),
tagName.Length);
Func<int, string, RazorError> errorFormatNoCloseAngle = (location, tagName) =>
new RazorError(
$"Missing close angle for tag helper '{tagName}'.",
new SourceLocation(location, 0, location),
tagName.Length);
// documentContent, expectedOutput, expectedErrors
return new TheoryData<string, MarkupBlock, RazorError[]>
{
{
"<p><strong>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("strong"))),
new[] { errorFormatUnclosed(1, "p"), errorFormatUnclosed(4, "strong") }
},
{
"<p><strong></strong>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("strong"))),
new[] { errorFormatUnclosed(1, "p") }
},
{
"<p><strong></p><strong>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("strong")),
blockFactory.MarkupTagBlock("<strong>")),
new[] { errorFormatUnclosed(4, "strong") }
},
{
"<<p><<strong></</strong</strong></p>",
new MarkupBlock(
blockFactory.MarkupTagBlock("<"),
new MarkupTagHelperBlock("p",
blockFactory.MarkupTagBlock("<"),
new MarkupTagHelperBlock("strong",
blockFactory.MarkupTagBlock("</")),
blockFactory.MarkupTagBlock("</strong>"))),
new[] { errorFormatNoCloseAngle(17, "strong"), errorFormatUnclosed(25, "strong") }
},
{
"<<p><<strong></</strong></strong></p>",
new MarkupBlock(
blockFactory.MarkupTagBlock("<"),
new MarkupTagHelperBlock("p",
blockFactory.MarkupTagBlock("<"),
new MarkupTagHelperBlock("strong",
blockFactory.MarkupTagBlock("</")),
blockFactory.MarkupTagBlock("</strong>"))),
new[] { errorFormatUnclosed(26, "strong") }
},
{
"<<p><<custom></<</custom></custom></p>",
new MarkupBlock(
blockFactory.MarkupTagBlock("<"),
new MarkupTagHelperBlock("p",
blockFactory.MarkupTagBlock("<"),
new MarkupTagHelperBlock("custom",
blockFactory.MarkupTagBlock("</"),
blockFactory.MarkupTagBlock("<")),
blockFactory.MarkupTagBlock("</custom>"))),
new[] { errorFormatUnclosed(27, "custom") }
},
};
}
}
[Theory]
[MemberData(nameof(PartialRequiredParentData))]
public void Rewrite_UnderstandsPartialRequiredParentTags(
string documentContent,
MarkupBlock expectedOutput,
RazorError[] expectedErrors)
{
// Arrange
var descriptors = new TagHelperDescriptor[]
{
new TagHelperDescriptor
{
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "p",
},
new TagHelperDescriptor
{
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "div",
},
new TagHelperDescriptor
{
TagName = "*",
TypeName = "CatchALlTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "p",
},
new TagHelperDescriptor
{
TagName = "p",
TypeName = "PTagHelper",
AssemblyName = "SomeAssembly"
}
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
// Act & Assert
EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors);
}
public static TheoryData NestedVoidSelfClosingRequiredParentData
{
get
{
var factory = CreateDefaultSpanFactory();
var blockFactory = new BlockFactory(factory);
// documentContent, expectedOutput
return new TheoryData<string, MarkupBlock>
{
{
"<input><strong></strong>",
new MarkupBlock(
new MarkupTagHelperBlock("input", TagMode.StartTagOnly),
blockFactory.MarkupTagBlock("<strong>"),
blockFactory.MarkupTagBlock("</strong>"))
},
{
"<p><input><strong></strong></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("input", TagMode.StartTagOnly),
new MarkupTagHelperBlock("strong")))
},
{
"<p><br><strong></strong></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
blockFactory.MarkupTagBlock("<br>"),
new MarkupTagHelperBlock("strong")))
},
{
"<p><p><br></p><strong></strong></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("p",
blockFactory.MarkupTagBlock("<br>")),
new MarkupTagHelperBlock("strong")))
},
{
"<input><strong></strong>",
new MarkupBlock(
new MarkupTagHelperBlock("input", TagMode.StartTagOnly),
blockFactory.MarkupTagBlock("<strong>"),
blockFactory.MarkupTagBlock("</strong>"))
},
{
"<p><input /><strong /></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("input", TagMode.SelfClosing),
new MarkupTagHelperBlock("strong", TagMode.SelfClosing)))
},
{
"<p><br /><strong /></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
blockFactory.MarkupTagBlock("<br />"),
new MarkupTagHelperBlock("strong", TagMode.SelfClosing)))
},
{
"<p><p><br /></p><strong /></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("p",
blockFactory.MarkupTagBlock("<br />")),
new MarkupTagHelperBlock("strong", TagMode.SelfClosing)))
},
};
}
}
[Theory]
[MemberData(nameof(NestedVoidSelfClosingRequiredParentData))]
public void Rewrite_UnderstandsNestedVoidSelfClosingRequiredParent(
string documentContent,
MarkupBlock expectedOutput)
{
// Arrange
var descriptors = new TagHelperDescriptor[]
{
new TagHelperDescriptor
{
TagName = "input",
TypeName = "InputTagHelper",
AssemblyName = "SomeAssembly",
TagStructure = TagStructure.WithoutEndTag,
},
new TagHelperDescriptor
{
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "p",
},
new TagHelperDescriptor
{
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "input",
},
new TagHelperDescriptor
{
TagName = "p",
TypeName = "PTagHelper",
AssemblyName = "SomeAssembly"
}
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
// Act & Assert
EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]);
}
public static TheoryData NestedRequiredParentData
{
get
{
var factory = CreateDefaultSpanFactory();
var blockFactory = new BlockFactory(factory);
// documentContent, expectedOutput
return new TheoryData<string, MarkupBlock>
{
{
"<strong></strong>",
new MarkupBlock(
blockFactory.MarkupTagBlock("<strong>"),
blockFactory.MarkupTagBlock("</strong>"))
},
{
"<p><strong></strong></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("strong")))
},
{
"<div><strong></strong></div>",
new MarkupBlock(
blockFactory.MarkupTagBlock("<div>"),
new MarkupTagHelperBlock("strong"),
blockFactory.MarkupTagBlock("</div>"))
},
{
"<strong><strong></strong></strong>",
new MarkupBlock(
blockFactory.MarkupTagBlock("<strong>"),
blockFactory.MarkupTagBlock("<strong>"),
blockFactory.MarkupTagBlock("</strong>"),
blockFactory.MarkupTagBlock("</strong>"))
},
{
"<p><strong><strong></strong></strong></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("strong",
blockFactory.MarkupTagBlock("<strong>"),
blockFactory.MarkupTagBlock("</strong>"))))
},
};
}
}
[Theory]
[MemberData(nameof(NestedRequiredParentData))]
public void Rewrite_UnderstandsNestedRequiredParent(string documentContent, MarkupBlock expectedOutput)
{
// Arrange
var descriptors = new TagHelperDescriptor[]
{
new TagHelperDescriptor
{
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "p",
},
new TagHelperDescriptor
{
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "div",
},
new TagHelperDescriptor
{
TagName = "p",
TypeName = "PTagHelper",
AssemblyName = "SomeAssembly"
}
};
var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
// Act & Assert
EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]);
}
[Fact]
public void Rewrite_UnderstandsTagHelperPrefixAndAllowedChildren()
{