diff --git a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs index 43ab78c1f8..5c731252c1 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs @@ -426,6 +426,22 @@ namespace Microsoft.AspNet.Razor.Runtime return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace"), p0, p1); } + /// + /// Parent Tag + /// + internal static string TagHelperDescriptorFactory_ParentTag + { + get { return GetString("TagHelperDescriptorFactory_ParentTag"); } + } + + /// + /// Parent Tag + /// + internal static string FormatTagHelperDescriptorFactory_ParentTag() + { + return GetString("TagHelperDescriptorFactory_ParentTag"); + } + 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 1c1593cc29..a1cdfa91b9 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx +++ b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx @@ -195,4 +195,7 @@ Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace. + + Parent Tag + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlTargetElementAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlTargetElementAttribute.cs index e1c1164222..2e2882dbfc 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlTargetElementAttribute.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlTargetElementAttribute.cs @@ -79,5 +79,11 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// /// public TagStructure TagStructure { get; set; } + + /// + /// The required HTML element name of the direct parent. + /// + /// A null value indicates any HTML element name is appropriate. + public string ParentTag { get; set; } } } \ 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 06b07b2b00..47eb1df056 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs @@ -142,6 +142,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers requiredAttributes: Enumerable.Empty(), 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 attributeDescriptors, IEnumerable requiredAttributes, IEnumerable 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; + } + + /// + /// Internal for unit testing. + /// + 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( diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs index 11d30e3667..c17fbb8cf4 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs @@ -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 }); diff --git a/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs index c01092ef36..90f01e0220 100644 --- a/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs +++ b/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs @@ -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) { diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs index 334dc12961..5c114bc789 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs @@ -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 VoidElements = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "area", + "base", + "br", + "col", + "command", + "embed", + "hr", + "img", + "input", + "keygen", + "link", + "meta", + "param", + "source", + "track", + "wbr" + }; + private TagHelperDescriptorProvider _provider; - private Stack _trackerStack; + private Stack _trackerStack; private TagHelperBlockTracker _currentTagHelperTracker; private Stack _blockStack; private BlockBuilder _currentBlock; + private string _currentParentTagName; public TagHelperParseTreeRewriter(TagHelperDescriptorProvider provider) { _provider = provider; - _trackerStack = new Stack(); + _trackerStack = new Stack(); _blockStack = new Stack(); } @@ -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 @

would cause an error because there's no // matching end

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()); + descriptors = _provider.GetDescriptors( + tagName, + attributeNames: Enumerable.Empty(), + 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 _prefixedAllowedChildren; public TagHelperBlockTracker(TagHelperBlockBuilder builder) + : base(builder.TagName, isTagHelper: true) { Builder = builder; diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs index 68646e9211..78ea922aae 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs @@ -163,6 +163,12 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// null indicates all children are allowed. public IEnumerable AllowedChildren { get; set; } + /// + /// Get the name of the HTML element required as the immediate parent. + /// + /// null indicates no restriction on parent tag. + public string RequiredParent { get; set; } + /// /// The expected tag structure. /// diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs index 0f2ae63a34..f60e659494 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs @@ -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( diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs index b4185f8edd..4506e4f6da 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs @@ -40,10 +40,14 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// The name of the HTML tag to match. Providing a '*' tag name /// retrieves catch-all s (descriptors that target every tag). /// Attributes the HTML element must contain to match. + /// The parent tag name of the given tag. /// s that apply to the given . /// Will return an empty if no s are /// found. - public IEnumerable GetDescriptors(string tagName, IEnumerable attributeNames) + public IEnumerable GetDescriptors( + string tagName, + IEnumerable 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 ApplyParentTagFilter( + IEnumerable descriptors, + string parentTagName) + { + return descriptors.Where(descriptor => + descriptor.RequiredParent == null || + string.Equals(parentTagName, descriptor.RequiredParent, StringComparison.OrdinalIgnoreCase)); + } + private IEnumerable ApplyRequiredAttributes( IEnumerable descriptors, IEnumerable attributeNames) diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs index 4fe8252beb..8d46aba19a 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs @@ -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 + { + { + 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 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 GetInvalidNameOrPrefixData( Func onNameError, string whitespaceErrorString, diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TestTagHelpers/TagHelperDescriptorFactoryTagHelpers.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TestTagHelpers/TagHelperDescriptorFactoryTagHelpers.cs index 69256a1ae9..34c50f6489 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TestTagHelpers/TagHelperDescriptorFactoryTagHelpers.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TestTagHelpers/TagHelperDescriptorFactoryTagHelpers.cs @@ -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 diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs index 0c0565fcb5..899bfe1741 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs @@ -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, // availableDescriptors + IEnumerable> // 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 availableDescriptors, + IEnumerable expectedDescriptors) + { + // Arrange + var provider = new TagHelperDescriptorProvider(availableDescriptors); + + // Act + var resolvedDescriptors = provider.GetDescriptors( + tagName, + attributeNames: Enumerable.Empty(), + 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()); + var resolvedDescriptors = provider.GetDescriptors( + tagName: "th", + attributeNames: Enumerable.Empty(), + 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()); - var retrievedDescriptorsSpan = provider.GetDescriptors("th2:span", attributeNames: Enumerable.Empty()); + var retrievedDescriptorsDiv = provider.GetDescriptors( + tagName: "th:div", + attributeNames: Enumerable.Empty(), + parentTagName: "p"); + var retrievedDescriptorsSpan = provider.GetDescriptors( + tagName: "th2:span", + attributeNames: Enumerable.Empty(), + 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()); - var retrievedDescriptorsSpan = provider.GetDescriptors("th:span", attributeNames: Enumerable.Empty()); + var retrievedDescriptorsDiv = provider.GetDescriptors( + tagName: "th:div", + attributeNames: Enumerable.Empty(), + parentTagName: "p"); + var retrievedDescriptorsSpan = provider.GetDescriptors( + tagName: "th:span", + attributeNames: Enumerable.Empty(), + 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()); + var retrievedDescriptors = provider.GetDescriptors( + tagName: "th:div", + attributeNames: Enumerable.Empty(), + 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()); + var retrievedDescriptorsDiv = provider.GetDescriptors( + tagName: "div", + attributeNames: Enumerable.Empty(), + 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()); + var retrievedDescriptors = provider.GetDescriptors( + tagName: "foo", + attributeNames: Enumerable.Empty(), + 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()); - var spanDescriptors = provider.GetDescriptors("span", attributeNames: Enumerable.Empty()); + var divDescriptors = provider.GetDescriptors( + tagName: "div", + attributeNames: Enumerable.Empty(), + parentTagName: "p"); + var spanDescriptors = provider.GetDescriptors( + tagName: "span", + attributeNames: Enumerable.Empty(), + 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()); + var retrievedDescriptors = provider.GetDescriptors( + tagName: "div", + attributeNames: Enumerable.Empty(), + parentTagName: "p"); // Assert var descriptor = Assert.Single(retrievedDescriptors); diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs index cdce725540..f5a94afc79 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs @@ -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 diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs index 37d61f6c49..ec695afd9c 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs @@ -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 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 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 + { + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong"))), + new[] { errorFormatUnclosed(1, "p"), errorFormatUnclosed(4, "strong") } + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong"))), + new[] { errorFormatUnclosed(1, "p") } + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong")), + blockFactory.MarkupTagBlock("")), + new[] { errorFormatUnclosed(4, "strong") } + }, + { + "<

<

", + new MarkupBlock( + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("strong", + blockFactory.MarkupTagBlock(""))), + new[] { errorFormatNoCloseAngle(17, "strong"), errorFormatUnclosed(25, "strong") } + }, + { + "<

<

", + new MarkupBlock( + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("strong", + blockFactory.MarkupTagBlock(""))), + new[] { errorFormatUnclosed(26, "strong") } + }, + + { + "<

<

", + new MarkupBlock( + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("custom", + blockFactory.MarkupTagBlock(""))), + 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 + { + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("input", TagMode.StartTagOnly), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("input", TagMode.StartTagOnly), + new MarkupTagHelperBlock("strong"))) + }, + { + "


", + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("
"), + new MarkupTagHelperBlock("strong"))) + }, + { + "


", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("
")), + new MarkupTagHelperBlock("strong"))) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("input", TagMode.StartTagOnly), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("input", TagMode.SelfClosing), + new MarkupTagHelperBlock("strong", TagMode.SelfClosing))) + }, + { + "


", + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("
"), + new MarkupTagHelperBlock("strong", TagMode.SelfClosing))) + }, + { + "


", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("
")), + 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 + { + { + "", + new MarkupBlock( + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong"))) + }, + { + "
", + new MarkupBlock( + blockFactory.MarkupTagBlock("
"), + new MarkupTagHelperBlock("strong"), + blockFactory.MarkupTagBlock("
")) + }, + { + "", + new MarkupBlock( + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")))) + }, + }; + } + } + + [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() {