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("")),
+ blockFactory.MarkupTagBlock(" "))),
+ new[] { errorFormatNoCloseAngle(17, "strong"), errorFormatUnclosed(25, "strong") }
+ },
+ {
+ "<<
",
+ new MarkupBlock(
+ blockFactory.MarkupTagBlock("<"),
+ new MarkupTagHelperBlock("p",
+ blockFactory.MarkupTagBlock("<"),
+ new MarkupTagHelperBlock("strong",
+ blockFactory.MarkupTagBlock("")),
+ blockFactory.MarkupTagBlock(""))),
+ new[] { errorFormatUnclosed(26, "strong") }
+ },
+
+ {
+ "<<<
",
+ new MarkupBlock(
+ blockFactory.MarkupTagBlock("<"),
+ new MarkupTagHelperBlock("p",
+ blockFactory.MarkupTagBlock("<"),
+ new MarkupTagHelperBlock("custom",
+ blockFactory.MarkupTagBlock(""),
+ blockFactory.MarkupTagBlock("<")),
+ 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()
{