From 465ff9713df193beb9ef707022943a5a144953e7 Mon Sep 17 00:00:00 2001
From: "N. Taylor Mullen"
Date: Thu, 13 Aug 2015 16:15:54 -0700
Subject: [PATCH] Add ability for `TagHelper`s to specify restricted children.
- Specifying the `RestrictChildrenAttribute` enables `TagHelper`s to only allow other `TagHelper`s targeting specified names to be in the children.
- Used the `null` value to indicate that `AllowedChildren` was not specified and therefore everything is allowed. This is the default.
- Added name verification to name values to ensure that no bad values pass through the system.
- Added parsing tests to validate a mixture of content generates errors when expected.
#255
---
.../Properties/Resources.Designer.cs | 32 ++
.../Resources.resx | 6 +
.../TagHelpers/RestrictChildrenAttribute.cs | 42 ++
.../TagHelpers/TagHelperDescriptorFactory.cs | 92 +++-
.../TagHelpers/TagHelperDescriptorResolver.cs | 1 +
...aseSensitiveTagHelperDescriptorComparer.cs | 17 +-
.../TagHelpers/TagHelperParseTreeRewriter.cs | 86 +++-
.../Properties/RazorResources.Designer.cs | 32 ++
.../RazorResources.resx | 6 +
.../TagHelpers/TagHelperDescriptor.cs | 17 +
.../TagHelpers/TagHelperDescriptorComparer.cs | 19 +-
.../TagHelperDescriptorFactoryTest.cs | 45 ++
.../TagHelperDescriptorResolverTest.cs | 5 +
.../CSharpTagHelperRenderingTest.cs | 3 +
...TagHelperAttributeValueCodeRendererTest.cs | 1 +
.../TagHelpers/TagHelperBlockRewriterTest.cs | 3 +
.../TagHelperDescriptorProviderTest.cs | 1 +
.../TagHelpers/TagHelperDescriptorTest.cs | 12 +
.../TagHelperParseTreeRewriterTest.cs | 453 ++++++++++++++++++
19 files changed, 859 insertions(+), 14 deletions(-)
create mode 100644 src/Microsoft.AspNet.Razor.Runtime/TagHelpers/RestrictChildrenAttribute.cs
diff --git a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs
index 5e61f5058a..46f60316bf 100644
--- a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs
@@ -394,6 +394,38 @@ namespace Microsoft.AspNet.Razor.Runtime
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperAttributeList_CannotAddAttribute"), p0, p1, p2, p3);
}
+ ///
+ /// Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character.
+ ///
+ internal static string TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName
+ {
+ get { return GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName"); }
+ }
+
+ ///
+ /// Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character.
+ ///
+ internal static string FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName(object p0, object p1, object p2, object p3)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName"), p0, p1, p2, p3);
+ }
+
+ ///
+ /// Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace.
+ ///
+ internal static string TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace
+ {
+ get { return GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace"); }
+ }
+
+ ///
+ /// Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace.
+ ///
+ internal static string FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace"), p0, p1);
+ }
+
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 c59fd6e955..e4d0edb677 100644
--- a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx
+++ b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx
@@ -189,4 +189,10 @@
Cannot add a {0} with inconsistent names. The {1} property '{2}' must match the location '{3}'.
+
+ Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character.
+
+
+ Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace.
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/RestrictChildrenAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/RestrictChildrenAttribute.cs
new file mode 100644
index 0000000000..aae957d5a5
--- /dev/null
+++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/RestrictChildrenAttribute.cs
@@ -0,0 +1,42 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
+{
+ ///
+ /// Restricts children of the 's element.
+ ///
+ /// Combining this attribute with a that specifies its
+ /// as will result in
+ /// this attribute being ignored.
+ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+ public class RestrictChildrenAttribute : Attribute
+ {
+ ///
+ /// Instantiates a new instance of the class.
+ ///
+ ///
+ /// The tag name of an element allowed as a child. Tag helpers must target the element.
+ ///
+ ///
+ /// Additional names of elements allowed as children. Tag helpers must target all such elements.
+ ///
+ public RestrictChildrenAttribute(string tagName, params string[] tagNames)
+ {
+ var concatenatedNames = new string[1 + tagNames.Length];
+ concatenatedNames[0] = tagName;
+
+ tagNames.CopyTo(concatenatedNames, 1);
+
+ ChildTagNames = concatenatedNames;
+ }
+
+ ///
+ /// Get the names of elements allowed as children. Tag helpers must target all such elements.
+ ///
+ public IEnumerable ChildTagNames { get; }
+ }
+}
diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs
index b69692ec9b..9d35198670 100644
--- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs
+++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs
@@ -61,6 +61,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
var attributeDescriptors = GetAttributeDescriptors(type, designTime, errorSink);
var targetElementAttributes = GetValidTargetElementAttributes(typeInfo, errorSink);
+ var allowedChildren = GetAllowedChildren(typeInfo, errorSink);
var tagHelperDescriptors =
BuildTagHelperDescriptors(
@@ -68,6 +69,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName,
attributeDescriptors,
targetElementAttributes,
+ allowedChildren,
designTime);
return tagHelperDescriptors.Distinct(TagHelperDescriptorComparer.Default);
@@ -87,6 +89,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
string assemblyName,
IEnumerable attributeDescriptors,
IEnumerable targetElementAttributes,
+ IEnumerable allowedChildren,
bool designTime)
{
TagHelperDesignTimeDescriptor typeDesignTimeDescriptor = null;
@@ -118,6 +121,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName,
attributeDescriptors,
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: allowedChildren,
tagStructure: default(TagStructure),
designTimeDescriptor: typeDesignTimeDescriptor)
};
@@ -130,14 +134,70 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName,
attributeDescriptors,
attribute,
+ allowedChildren,
typeDesignTimeDescriptor));
}
+ private static IEnumerable GetAllowedChildren(TypeInfo typeInfo, ErrorSink errorSink)
+ {
+ var restrictChildrenAttribute = typeInfo.GetCustomAttribute(inherit: false);
+ if (restrictChildrenAttribute == null)
+ {
+ return null;
+ }
+
+ var allowedChildren = restrictChildrenAttribute.ChildTagNames;
+ var validAllowedChildren = GetValidAllowedChildren(allowedChildren, typeInfo.FullName, errorSink);
+
+ if (validAllowedChildren.Any())
+ {
+ return validAllowedChildren;
+ }
+ else
+ {
+ // All allowed children were invalid, return null to indicate that any child is acceptable.
+ return null;
+ }
+ }
+
+ // Internal for unit testing
+ internal static IEnumerable GetValidAllowedChildren(
+ IEnumerable allowedChildren,
+ string tagHelperName,
+ ErrorSink errorSink)
+ {
+ var validAllowedChildren = new List();
+
+ foreach (var name in allowedChildren)
+ {
+ var valid = TryValidateName(
+ name,
+ whitespaceError: Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace(
+ nameof(RestrictChildrenAttribute),
+ tagHelperName),
+ characterErrorBuilder: (invalidCharacter) =>
+ Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName(
+ nameof(RestrictChildrenAttribute),
+ name,
+ tagHelperName,
+ invalidCharacter),
+ errorSink: errorSink);
+
+ if (valid)
+ {
+ validAllowedChildren.Add(name);
+ }
+ }
+
+ return validAllowedChildren;
+ }
+
private static TagHelperDescriptor BuildTagHelperDescriptor(
string typeName,
string assemblyName,
IEnumerable attributeDescriptors,
TargetElementAttribute targetElementAttribute,
+ IEnumerable allowedChildren,
TagHelperDesignTimeDescriptor designTimeDescriptor)
{
var requiredAttributes = GetCommaSeparatedValues(targetElementAttribute.Attributes);
@@ -148,6 +208,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName,
attributeDescriptors,
requiredAttributes,
+ allowedChildren,
targetElementAttribute.TagStructure,
designTimeDescriptor);
}
@@ -158,6 +219,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
string assemblyName,
IEnumerable attributeDescriptors,
IEnumerable requiredAttributes,
+ IEnumerable allowedChildren,
TagStructure tagStructure,
TagHelperDesignTimeDescriptor designTimeDescriptor)
{
@@ -168,6 +230,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: assemblyName,
attributes: attributeDescriptors,
requiredAttributes: requiredAttributes,
+ allowedChildren: allowedChildren,
tagStructure: tagStructure,
designTimeDescriptor: designTimeDescriptor);
}
@@ -230,13 +293,28 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
var targetName = targetingAttributes ?
Resources.TagHelperDescriptorFactory_Attribute :
Resources.TagHelperDescriptorFactory_Tag;
+
+ var validName = TryValidateName(
+ name,
+ whitespaceError: Resources.FormatTargetElementAttribute_NameCannotBeNullOrWhitespace(targetName),
+ characterErrorBuilder: (invalidCharacter) =>
+ Resources.FormatTargetElementAttribute_InvalidName(targetName.ToLower(), name, invalidCharacter),
+ errorSink: errorSink);
+
+ return validName;
+ }
+
+ private static bool TryValidateName(
+ string name,
+ string whitespaceError,
+ Func characterErrorBuilder,
+ ErrorSink errorSink)
+ {
var validName = true;
if (string.IsNullOrWhiteSpace(name))
{
- errorSink.OnError(
- SourceLocation.Zero,
- Resources.FormatTargetElementAttribute_NameCannotBeNullOrWhitespace(targetName));
+ errorSink.OnError(SourceLocation.Zero, whitespaceError);
validName = false;
}
@@ -247,12 +325,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
if (char.IsWhiteSpace(character) ||
InvalidNonWhitespaceNameCharacters.Contains(character))
{
- errorSink.OnError(
- SourceLocation.Zero,
- Resources.FormatTargetElementAttribute_InvalidName(
- targetName.ToLower(),
- name,
- character));
+ var error = characterErrorBuilder(character);
+ errorSink.OnError(SourceLocation.Zero, error);
validName = false;
}
diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs
index 4256deb946..7b414470eb 100644
--- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs
+++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs
@@ -154,6 +154,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
descriptor.AssemblyName,
descriptor.Attributes,
descriptor.RequiredAttributes,
+ descriptor.AllowedChildren,
descriptor.TagStructure,
descriptor.DesignTimeDescriptor));
}
diff --git a/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs
index 39998465c9..2d262ce832 100644
--- a/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs
+++ b/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs
@@ -26,14 +26,19 @@ namespace Microsoft.AspNet.Razor.Test.Internal
}
return base.Equals(descriptorX, descriptorY) &&
- // Normal comparer doesn't care about the case, required attribute order, attributes or prefixes.
- // In tests we do.
+ // Normal comparer doesn't care about the case, required attribute order, allowed children order,
+ // attributes or prefixes. In tests we do.
string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.Ordinal) &&
string.Equals(descriptorX.Prefix, descriptorY.Prefix, StringComparison.Ordinal) &&
Enumerable.SequenceEqual(
descriptorX.RequiredAttributes,
descriptorY.RequiredAttributes,
StringComparer.Ordinal) &&
+ (descriptorX.AllowedChildren == descriptorY.AllowedChildren ||
+ Enumerable.SequenceEqual(
+ descriptorX.AllowedChildren,
+ descriptorY.AllowedChildren,
+ StringComparer.Ordinal)) &&
descriptorX.Attributes.SequenceEqual(
descriptorY.Attributes,
TagHelperAttributeDescriptorComparer.Default) &&
@@ -60,6 +65,14 @@ namespace Microsoft.AspNet.Razor.Test.Internal
hashCodeCombiner.Add(requiredAttribute, StringComparer.Ordinal);
}
+ if (descriptor.AllowedChildren != null)
+ {
+ foreach (var child in descriptor.AllowedChildren)
+ {
+ hashCodeCombiner.Add(child, StringComparer.Ordinal);
+ }
+ }
+
foreach (var attribute in descriptor.Attributes)
{
hashCodeCombiner.Add(TagHelperAttributeDescriptorComparer.Default.GetHashCode(attribute));
diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs
index 4b2641f8c4..7f410bf775 100644
--- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs
+++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs
@@ -4,6 +4,7 @@
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;
@@ -16,6 +17,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
{
private TagHelperDescriptorProvider _provider;
private Stack _trackerStack;
+ private TagHelperBlockTracker _currentTagHelperTracker;
private Stack _blockStack;
private BlockBuilder _currentBlock;
@@ -56,6 +58,11 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
{
continue;
}
+ else
+ {
+ // Non-TagHelper tag.
+ ValidateParentTagHelperAllowsPlainTag(childBlock, context.ErrorSink);
+ }
// If we get to here it means that we're a normal html tag. No need to iterate any deeper into
// the children of it because they wont be tag helpers.
@@ -67,6 +74,10 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
continue;
}
}
+ else
+ {
+ ValidateParentTagHelperAllowsContent((Span)child, context.ErrorSink);
+ }
// At this point the child is a Span or Block with Type BlockType.Tag that doesn't happen to be a
// tag helper.
@@ -108,7 +119,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
return false;
}
- var tracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null;
+ var tracker = _currentTagHelperTracker;
var tagNameScope = tracker?.Builder.TagName ?? string.Empty;
if (!IsEndTag(tagBlock))
@@ -135,6 +146,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
return false;
}
+ ValidateParentTagHelperAllowsTagHelper(tagName, tagBlock, context.ErrorSink);
ValidateDescriptors(descriptors, tagName, tagBlock, context.ErrorSink);
// We're in a start TagHelper block.
@@ -211,6 +223,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
// can't recover it means there was no corresponding tag helper start tag.
if (TryRecoverTagHelper(tagName, tagBlock, context))
{
+ ValidateParentTagHelperAllowsTagHelper(tagName, tagBlock, context.ErrorSink);
ValidateTagSyntax(tagName, tagBlock, context);
// Successfully recovered, move onto the next element.
@@ -267,6 +280,63 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
return attributeNames;
}
+ private void ValidateParentTagHelperAllowsContent(Span child, ErrorSink errorSink)
+ {
+ var allowedChildren = _currentTagHelperTracker?.AllowedChildren;
+ if (allowedChildren != null)
+ {
+ var content = child.Content;
+ if (!string.IsNullOrWhiteSpace(content))
+ {
+ var trimmedStart = content.TrimStart();
+ var whitespace = content.Substring(0, content.Length - trimmedStart.Length);
+ var errorStart = SourceLocation.Advance(child.Start, whitespace);
+ var length = trimmedStart.TrimEnd().Length;
+ var allowedChildrenString = string.Join(", ", allowedChildren);
+ errorSink.OnError(
+ errorStart,
+ RazorResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent(
+ _currentTagHelperTracker.Builder.TagName,
+ allowedChildrenString),
+ length);
+ }
+ }
+ }
+
+ private void ValidateParentTagHelperAllowsPlainTag(Block tagBlock, ErrorSink errorSink)
+ {
+ if (_currentTagHelperTracker?.AllowedChildren != null)
+ {
+ OnAllowedChildrenTagError(_currentTagHelperTracker, tagBlock, errorSink);
+ }
+ }
+
+ private void ValidateParentTagHelperAllowsTagHelper(string tagName, Block tagBlock, ErrorSink errorSink)
+ {
+ var currentlyAllowedChildren = _currentTagHelperTracker?.AllowedChildren;
+
+ if (currentlyAllowedChildren != null &&
+ !currentlyAllowedChildren.Contains(tagName, StringComparer.OrdinalIgnoreCase))
+ {
+ OnAllowedChildrenTagError(_currentTagHelperTracker, tagBlock, errorSink);
+ }
+ }
+
+ private static void OnAllowedChildrenTagError(
+ TagHelperBlockTracker tracker,
+ Block tagBlock,
+ ErrorSink errorSink)
+ {
+ var tagName = GetTagName(tagBlock);
+ var allowedChildrenString = string.Join(", ", tracker.AllowedChildren);
+ var errorMessage = RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag(
+ tagName,
+ tracker.Builder.TagName,
+ allowedChildrenString);
+
+ errorSink.OnError(tagBlock.Start, errorMessage, tagBlock.Length);
+ }
+
private static void ValidateDescriptors(
IEnumerable descriptors,
string tagName,
@@ -361,6 +431,8 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
// for formatting.
_trackerStack.Pop().Builder.SourceEndTag = endTag;
+ _currentTagHelperTracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null;
+
BuildCurrentlyTrackedBlock();
}
@@ -385,7 +457,8 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
private void TrackTagHelperBlock(TagHelperBlockBuilder builder)
{
- _trackerStack.Push(new TagHelperBlockTracker(builder));
+ _currentTagHelperTracker = new TagHelperBlockTracker(builder);
+ _trackerStack.Push(_currentTagHelperTracker);
TrackBlock(builder);
}
@@ -478,11 +551,20 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
public TagHelperBlockTracker(TagHelperBlockBuilder builder)
{
Builder = builder;
+
+ if (Builder.Descriptors.Any(descriptor => descriptor.AllowedChildren != null))
+ {
+ AllowedChildren = Builder.Descriptors
+ .SelectMany(descriptor => descriptor.AllowedChildren)
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+ }
}
public TagHelperBlockBuilder Builder { get; }
public uint OpenMatchingTags { get; set; }
+
+ public IEnumerable AllowedChildren { get; }
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs
index 3677d1371c..5d12ab827d 100644
--- a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs
+++ b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs
@@ -1514,6 +1514,38 @@ namespace Microsoft.AspNet.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag"), p0, p1, p2);
}
+ ///
+ /// The parent <{0}> tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed.
+ ///
+ internal static string TagHelperParseTreeRewriter_CannotHaveNonTagContent
+ {
+ get { return GetString("TagHelperParseTreeRewriter_CannotHaveNonTagContent"); }
+ }
+
+ ///
+ /// The parent <{0}> tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed.
+ ///
+ internal static string FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_CannotHaveNonTagContent"), p0, p1);
+ }
+
+ ///
+ /// The <{0}> tag is not allowed by parent <{1}> tag helper. Only child tag helper(s) targeting tag name(s) '{2}' are allowed.
+ ///
+ internal static string TagHelperParseTreeRewriter_InvalidNestedTag
+ {
+ get { return GetString("TagHelperParseTreeRewriter_InvalidNestedTag"); }
+ }
+
+ ///
+ /// The <{0}> tag is not allowed by parent <{1}> tag helper. Only child tag helper(s) targeting tag name(s) '{2}' are allowed.
+ ///
+ internal static string FormatTagHelperParseTreeRewriter_InvalidNestedTag(object p0, object p1, object p2)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_InvalidNestedTag"), p0, p1, p2);
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Razor/RazorResources.resx b/src/Microsoft.AspNet.Razor/RazorResources.resx
index d0680dfa81..d099d3330c 100644
--- a/src/Microsoft.AspNet.Razor/RazorResources.resx
+++ b/src/Microsoft.AspNet.Razor/RazorResources.resx
@@ -419,4 +419,10 @@ Instead, wrap the contents of the block in "{{}}":
Found an end tag (</{0}>) for tag helper '{1}' with tag structure that disallows an end tag ('{2}').
+
+ The parent <{0}> tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed.
+
+
+ The <{0}> tag is not allowed by parent <{1}> tag helper. Only child tag helper(s) targeting tag name(s) '{2}' are allowed.
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs
index 4f2ebd329b..6c1d94d3dc 100644
--- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs
+++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs
@@ -61,6 +61,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
assemblyName: assemblyName,
attributes: attributes,
requiredAttributes: requiredAttributes,
+ allowedChildren: null,
tagStructure: TagStructure.Unspecified,
designTimeDescriptor: null)
{
@@ -84,6 +85,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
///
/// The attribute names required for the tag helper to target the HTML tag.
///
+ ///
+ /// The names of elements allowed as children. Tag helpers must target all such elements.
+ ///
+ /// The expected tag structure.
/// The that contains design
/// time information about the tag helper.
public TagHelperDescriptor(
@@ -93,6 +98,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
[NotNull] string assemblyName,
[NotNull] IEnumerable attributes,
[NotNull] IEnumerable requiredAttributes,
+ IEnumerable allowedChildren,
TagStructure tagStructure,
TagHelperDesignTimeDescriptor designTimeDescriptor)
{
@@ -105,6 +111,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers
RequiredAttributes = new List(requiredAttributes);
TagStructure = tagStructure;
DesignTimeDescriptor = designTimeDescriptor;
+
+ if (allowedChildren != null)
+ {
+ AllowedChildren = new List(allowedChildren);
+ }
}
///
@@ -147,6 +158,12 @@ namespace Microsoft.AspNet.Razor.TagHelpers
///
public IReadOnlyList RequiredAttributes { get; }
+ ///
+ /// Get the names of elements allowed as children. Tag helpers must target all such elements.
+ ///
+ /// null indicates all children are allowed.
+ public IReadOnlyList AllowedChildren { get; }
+
///
/// The expected tag structure.
///
diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs
index 88cb6bcb6f..1fff142a56 100644
--- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs
+++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs
@@ -30,7 +30,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
///
/// Determines equality based on ,
/// , ,
- /// , and .
+ /// , ,
+ /// and .
/// Ignores because it can be inferred directly from
/// and .
///
@@ -49,6 +50,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
descriptorX.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase),
descriptorY.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase),
StringComparer.OrdinalIgnoreCase) &&
+ (descriptorX.AllowedChildren == descriptorY.AllowedChildren ||
+ (descriptorX.AllowedChildren != null &&
+ descriptorY.AllowedChildren != null &&
+ Enumerable.SequenceEqual(
+ descriptorX.AllowedChildren.OrderBy(child => child, StringComparer.OrdinalIgnoreCase),
+ descriptorY.AllowedChildren.OrderBy(child => child, StringComparer.OrdinalIgnoreCase),
+ StringComparer.OrdinalIgnoreCase))) &&
descriptorX.TagStructure == descriptorY.TagStructure;
}
@@ -69,6 +77,15 @@ namespace Microsoft.AspNet.Razor.TagHelpers
hashCodeCombiner.Add(attribute, StringComparer.OrdinalIgnoreCase);
}
+ if (descriptor.AllowedChildren != null)
+ {
+ var allowedChildren = descriptor.AllowedChildren.OrderBy(child => child, StringComparer.OrdinalIgnoreCase);
+ foreach (var child in allowedChildren)
+ {
+ hashCodeCombiner.Add(child, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
return hashCodeCombiner.CombinedHash;
}
}
diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs
index 6cebe4c8f1..b73db12361 100644
--- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs
+++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs
@@ -35,6 +35,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null)
}
@@ -50,6 +51,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null),
new TagHelperDescriptor(
@@ -59,6 +61,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.NormalOrSelfClosing,
designTimeDescriptor: null),
}
@@ -74,6 +77,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null),
new TagHelperDescriptor(
@@ -83,6 +87,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.Unspecified,
designTimeDescriptor: null),
}
@@ -137,6 +142,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: new TagHelperDesignTimeDescriptor
{
@@ -155,6 +161,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: new TagHelperDesignTimeDescriptor
{
@@ -167,6 +174,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: new TagHelperDesignTimeDescriptor
{
@@ -1734,6 +1742,42 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
}
}
+ public static TheoryData InvalidRestrictChildrenNameData
+ {
+ get
+ {
+ var nullOrWhiteSpaceError =
+ Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace(
+ nameof(RestrictChildrenAttribute),
+ "SomeTagHelper");
+
+ return GetInvalidNameOrPrefixData(
+ onNameError: (invalidInput, invalidCharacter) =>
+ Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName(
+ nameof(RestrictChildrenAttribute),
+ invalidInput,
+ "SomeTagHelper",
+ invalidCharacter),
+ whitespaceErrorString: nullOrWhiteSpaceError,
+ onDataError: null);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidRestrictChildrenNameData))]
+ public void GetValidAllowedChildren_AddsExpectedErrors(string name, string[] expectedErrorMessages)
+ {
+ // Arrange
+ var errorSink = new ErrorSink();
+ var expectedErrors = expectedErrorMessages.Select(message => new RazorError(message, SourceLocation.Zero));
+
+ // Act
+ TagHelperDescriptorFactory.GetValidAllowedChildren(new[] { name }, "SomeTagHelper", errorSink);
+
+ // Assert
+ Assert.Equal(expectedErrors, errorSink.Errors);
+ }
+
private static TheoryData GetInvalidNameOrPrefixData(
Func onNameError,
string whitespaceErrorString,
@@ -1973,6 +2017,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: assemblyName,
attributes: attributes ?? Enumerable.Empty(),
requiredAttributes: requiredAttributes ?? Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
}
diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs
index 8241d3d20a..d7c1f7962f 100644
--- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs
+++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs
@@ -32,6 +32,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
}
@@ -48,6 +49,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
}
@@ -609,6 +611,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: assemblyB,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
@@ -1045,6 +1048,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: assemblyB,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
@@ -1444,6 +1448,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName,
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
}
diff --git a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs
index 21e80eea24..7bad3b0993 100644
--- a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs
+++ b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs
@@ -1550,6 +1550,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
new TagHelperAttributeDescriptor("age", pAgePropertyInfo)
},
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.NormalOrSelfClosing,
designTimeDescriptor: null),
new TagHelperDescriptor(
@@ -1562,6 +1563,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo)
},
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null),
new TagHelperDescriptor(
@@ -1575,6 +1577,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
new TagHelperAttributeDescriptor("checked", checkedPropertyInfo)
},
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.Unspecified,
designTimeDescriptor: null)
};
diff --git a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs
index c5ee79ba70..21bb234948 100644
--- a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs
+++ b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs
@@ -34,6 +34,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo)
},
requiredAttributes: new string[0],
+ allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null),
new TagHelperDescriptor(
diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs
index aba10bf28b..55187f361b 100644
--- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs
+++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs
@@ -87,6 +87,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[0],
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null)
};
@@ -189,6 +190,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[0],
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: structure1,
designTimeDescriptor: null),
new TagHelperDescriptor(
@@ -198,6 +200,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[0],
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: structure2,
designTimeDescriptor: null)
};
diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs
index 2addac6796..b91ad18783 100644
--- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs
+++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs
@@ -322,6 +322,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
}
diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs
index 3471341097..a677fb3c6e 100644
--- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs
+++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs
@@ -23,6 +23,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
assemblyName: "assembly name",
attributes: Enumerable.Empty(),
requiredAttributes: new[] { "required attribute one", "required attribute two" },
+ allowedChildren: new[] { "allowed child one" },
tagStructure: TagStructure.Unspecified,
designTimeDescriptor: new TagHelperDesignTimeDescriptor
{
@@ -40,6 +41,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" +
"[\"required attribute one\",\"required attribute two\"]," +
+ $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\"]," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":{{"+
$"\"{ nameof(TagHelperDesignTimeDescriptor.Summary) }\":\"usage summary\"," +
@@ -78,6 +80,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
designTimeDescriptor: null),
},
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.NormalOrSelfClosing,
designTimeDescriptor: null);
var expectedSerializedDescriptor =
@@ -100,6 +103,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
+ $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":1," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}";
@@ -135,6 +139,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
designTimeDescriptor: null),
},
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: new[] { "allowed child one", "allowed child two" },
tagStructure: default(TagStructure),
designTimeDescriptor: null);
var expectedSerializedDescriptor =
@@ -157,6 +162,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
+ $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}";
@@ -180,6 +186,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{nameof(TagHelperDescriptor.Attributes)}\":[]," +
$"\"{nameof(TagHelperDescriptor.RequiredAttributes)}\":" +
"[\"required attribute one\",\"required attribute two\"]," +
+ $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," +
$"\"{nameof(TagHelperDescriptor.TagStructure)}\":2," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":{{" +
$"\"{ nameof(TagHelperDesignTimeDescriptor.Summary) }\":\"usage summary\"," +
@@ -192,6 +199,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
assemblyName: "assembly name",
attributes: Enumerable.Empty(),
requiredAttributes: new[] { "required attribute one", "required attribute two" },
+ allowedChildren: new[] { "allowed child one", "allowed child two" },
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: new TagHelperDesignTimeDescriptor
{
@@ -242,6 +250,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
+ $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," +
$"\"{nameof(TagHelperDescriptor.TagStructure)}\":0," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}";
var expectedDescriptor = new TagHelperDescriptor(
@@ -265,6 +274,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
designTimeDescriptor: null),
},
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.Unspecified,
designTimeDescriptor: null);
@@ -331,6 +341,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
+ $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," +
$"\"{nameof(TagHelperDescriptor.TagStructure)}\":1," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}";
var expectedDescriptor = new TagHelperDescriptor(
@@ -354,6 +365,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
designTimeDescriptor: null),
},
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.NormalOrSelfClosing,
designTimeDescriptor: null);
diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs
index b9872db90e..c426a3a105 100644
--- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs
+++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs
@@ -18,6 +18,450 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
{
public class TagHelperParseTreeRewriterTest : TagHelperRewritingTestBase
{
+ [Fact]
+ public void Rewrite_CanHandleInvalidChildrenWithWhitespace()
+ {
+ // Arrange
+ var factory = CreateDefaultSpanFactory();
+ var blockFactory = new BlockFactory(factory);
+ var documentContent = $"{Environment.NewLine} {Environment.NewLine} Hello" +
+ $"{Environment.NewLine} {Environment.NewLine}
";
+ var expectedErrors = new[] {
+ new RazorError(
+ RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag("strong", "p", "br"),
+ absoluteIndex: 9,
+ lineIndex: 1,
+ columnIndex: 9,
+ length: 8),
+ new RazorError(
+ RazorResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent("p", "br"),
+ absoluteIndex: 27,
+ lineIndex: 2,
+ columnIndex: 27,
+ length: 5),
+ new RazorError(
+ RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag("strong", "p", "br"),
+ absoluteIndex: 38,
+ lineIndex: 3,
+ columnIndex: 38,
+ length: 9),
+ };
+ var expectedOutput = new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ factory.Markup(Environment.NewLine + " "),
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup(Environment.NewLine + " Hello" + Environment.NewLine + " "),
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup(Environment.NewLine)));
+ var descriptors = new TagHelperDescriptor[]
+ {
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "p",
+ typeName: "PTagHelper",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: new[] { "br" },
+ tagStructure: TagStructure.Unspecified,
+ designTimeDescriptor: null),
+ };
+ var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
+
+ // Act & Assert
+ EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors);
+ }
+
+ [Fact]
+ public void Rewrite_RecoversWhenRequiredAttributeMismatchAndRestrictedChildren()
+ {
+ // Arrange
+ var factory = CreateDefaultSpanFactory();
+ var blockFactory = new BlockFactory(factory);
+ var documentContent = "";
+
+ var expectedErrors = new[] {
+ new RazorError(
+ RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag("strong", "strong", "br"),
+ absoluteIndex: 17,
+ lineIndex: 0,
+ columnIndex: 17,
+ length: 8),
+ new RazorError(
+ RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag("strong", "strong", "br"),
+ absoluteIndex: 25,
+ lineIndex: 0,
+ columnIndex: 25,
+ length: 9),
+ };
+ var expectedOutput = new MarkupBlock(
+ new MarkupTagHelperBlock("strong",
+ new List>
+ {
+ new KeyValuePair("required", null)
+ },
+ blockFactory.MarkupTagBlock(""),
+ blockFactory.MarkupTagBlock("")));
+ var descriptors = new TagHelperDescriptor[]
+ {
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "strong",
+ typeName: "StrongTagHelper",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: new[] { "required" },
+ allowedChildren: new[] { "br" },
+ tagStructure: TagStructure.Unspecified,
+ designTimeDescriptor: null),
+ };
+ var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
+
+ // Act & Assert
+ EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors);
+ }
+
+ [Fact]
+ public void Rewrite_CanHandleMultipleTagHelpersWithAllowedChildren_OneNull()
+ {
+ // Arrange
+ var factory = CreateDefaultSpanFactory();
+ var documentContent = "Hello World
";
+ var expectedOutput = new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new MarkupTagHelperBlock("strong",
+ factory.Markup("Hello World")),
+ new MarkupTagHelperBlock("br", TagMode.StartTagOnly)));
+ var descriptors = new TagHelperDescriptor[]
+ {
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "p",
+ typeName: "PTagHelper1",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: new[] { "strong", "br" },
+ tagStructure: TagStructure.Unspecified,
+ designTimeDescriptor: null),
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "p",
+ typeName: "PTagHelper2",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
+ tagStructure: TagStructure.Unspecified,
+ designTimeDescriptor: null),
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "strong",
+ typeName: "StrongTagHelper",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
+ tagStructure: TagStructure.Unspecified,
+ designTimeDescriptor: null),
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "br",
+ typeName: "BRTagHelper",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
+ tagStructure: TagStructure.WithoutEndTag,
+ designTimeDescriptor: null),
+ };
+ var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
+
+ // Act & Assert
+ EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]);
+ }
+
+ [Fact]
+ public void Rewrite_CanHandleMultipleTagHelpersWithAllowedChildren()
+ {
+ // Arrange
+ var factory = CreateDefaultSpanFactory();
+ var documentContent = "Hello World
";
+ var expectedOutput = new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new MarkupTagHelperBlock("strong",
+ factory.Markup("Hello World")),
+ new MarkupTagHelperBlock("br", TagMode.StartTagOnly)));
+ var descriptors = new TagHelperDescriptor[]
+ {
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "p",
+ typeName: "PTagHelper1",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: new[] { "strong" },
+ tagStructure: TagStructure.Unspecified,
+ designTimeDescriptor: null),
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "p",
+ typeName: "PTagHelper2",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: new[] { "br" },
+ tagStructure: TagStructure.Unspecified,
+ designTimeDescriptor: null),
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "strong",
+ typeName: "StrongTagHelper",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
+ tagStructure: TagStructure.Unspecified,
+ designTimeDescriptor: null),
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "br",
+ typeName: "BRTagHelper",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
+ tagStructure: TagStructure.WithoutEndTag,
+ designTimeDescriptor: null),
+ };
+ var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
+
+ // Act & Assert
+ EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]);
+ }
+
+ public static TheoryData AllowedChildrenData
+ {
+ get
+ {
+ var factory = CreateDefaultSpanFactory();
+ var blockFactory = new BlockFactory(factory);
+ Func nestedTagError =
+ (childName, parentName, allowed, location, length) => new RazorError(
+ RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag(
+ childName,
+ parentName,
+ allowed),
+ absoluteIndex: location,
+ lineIndex: 0,
+ columnIndex: location,
+ length: length);
+ Func nestedContentError =
+ (parentName, allowed, location, length) => new RazorError(
+ RazorResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent(parentName, allowed),
+ absoluteIndex: location,
+ lineIndex: 0,
+ columnIndex: location,
+ length: length);
+
+ return new TheoryData, MarkupBlock, RazorError[]>
+ {
+ {
+ "
",
+ new[] { "br" },
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new MarkupTagHelperBlock("br", TagMode.SelfClosing))),
+ new RazorError[0]
+ },
+ {
+ $"{Environment.NewLine}
{Environment.NewLine}
",
+ new[] { "br" },
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ factory.Markup(Environment.NewLine),
+ new MarkupTagHelperBlock("br", TagMode.SelfClosing),
+ factory.Markup(Environment.NewLine))),
+ new RazorError[0]
+ },
+ {
+ "
",
+ new[] { "strong" },
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new MarkupTagHelperBlock("br", TagMode.StartTagOnly))),
+ new[] { nestedTagError("br", "p", "strong", 3, 4) }
+ },
+ {
+ "Hello
",
+ new[] { "strong" },
+ new MarkupBlock(new MarkupTagHelperBlock("p", factory.Markup("Hello"))),
+ new[] { nestedContentError("p", "strong", 3, 5) }
+ },
+ {
+ "
",
+ new[] { "br", "strong" },
+ new MarkupBlock(new MarkupTagHelperBlock("p", blockFactory.MarkupTagBlock("
"))),
+ new[] { nestedTagError("hr", "p", "br, strong", 3, 6) }
+ },
+ {
+ "
Hello
",
+ new[] { "strong" },
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new MarkupTagHelperBlock("br", TagMode.StartTagOnly),
+ factory.Markup("Hello"))),
+ new[] { nestedTagError("br", "p", "strong", 3, 4), nestedContentError("p", "strong", 7, 5) }
+ },
+ {
+ "Title:
Something
",
+ new[] { "strong" },
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new MarkupTagHelperBlock("strong", factory.Markup("Title:")),
+ new MarkupTagHelperBlock("br", TagMode.SelfClosing),
+ factory.Markup("Something"))),
+ new[]
+ {
+ nestedContentError("strong", "strong", 11, 6),
+ nestedTagError("br", "p", "strong", 26, 6),
+ nestedContentError("p", "strong", 32, 9),
+ }
+ },
+ {
+ "Title:
Something
",
+ new[] { "strong", "br" },
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new MarkupTagHelperBlock("strong", factory.Markup("Title:")),
+ new MarkupTagHelperBlock("br", TagMode.SelfClosing),
+ factory.Markup("Something"))),
+ new[]
+ {
+ nestedContentError("strong", "strong, br", 11, 6),
+ nestedContentError("p", "strong, br", 32, 9),
+ }
+ },
+ {
+ " Title:
Something
",
+ new[] { "strong", "br" },
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ factory.Markup(" "),
+ new MarkupTagHelperBlock("strong", factory.Markup("Title:")),
+ factory.Markup(" "),
+ new MarkupTagHelperBlock("br", TagMode.SelfClosing),
+ factory.Markup(" Something"))),
+ new[]
+ {
+ nestedContentError("strong", "strong, br", 13, 6),
+ nestedContentError("p", "strong, br", 38, 9),
+ }
+ },
+ {
+ "Title:
A Very Cool
Something
",
+ new[] { "strong" },
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new MarkupTagHelperBlock("strong",
+ factory.Markup("Title:"),
+ new MarkupTagHelperBlock("br", TagMode.StartTagOnly),
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup("A Very Cool"),
+ blockFactory.MarkupTagBlock("")),
+ new MarkupTagHelperBlock("br", TagMode.SelfClosing),
+ factory.Markup("Something"))),
+ new[]
+ {
+ nestedContentError("strong", "strong", 11, 6),
+ nestedTagError("br", "strong", "strong", 17, 4),
+ nestedTagError("em", "strong", "strong", 21, 4),
+ nestedContentError("strong", "strong", 25, 11),
+ nestedTagError("em", "strong", "strong", 36, 5),
+ nestedTagError("br", "p", "strong", 50, 6),
+ nestedContentError("p", "strong", 56, 9)
+ }
+ },
+ {
+ "Title:
A Very Cool
Something
",
+ new[] { "custom" },
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup("Title:"),
+ new MarkupTagHelperBlock("br", TagMode.StartTagOnly),
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup("A Very Cool"),
+ blockFactory.MarkupTagBlock(""),
+ blockFactory.MarkupTagBlock(""),
+ new MarkupTagHelperBlock("br", TagMode.SelfClosing),
+ factory.Markup("Something"))),
+ new[]
+ {
+ nestedTagError("custom", "p", "custom", 3, 8),
+ nestedContentError("p", "custom", 11, 6),
+ nestedTagError("br", "p", "custom", 17, 4),
+ nestedTagError("em", "p", "custom", 21, 4),
+ nestedContentError("p", "custom", 25, 11),
+ nestedTagError("em", "p", "custom", 36, 5),
+ nestedTagError("custom", "p", "custom", 41, 9),
+ nestedTagError("br", "p", "custom", 50, 6),
+ nestedContentError("p", "custom", 56, 9)
+ }
+ }
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(AllowedChildrenData))]
+ public void Rewrite_UnderstandsAllowedChildren(
+ string documentContent,
+ IEnumerable allowedChildren,
+ MarkupBlock expectedOutput,
+ RazorError[] expectedErrors)
+ {
+ // Arrange
+ var descriptors = new TagHelperDescriptor[]
+ {
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "p",
+ typeName: "PTagHelper",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: allowedChildren,
+ tagStructure: TagStructure.Unspecified,
+ designTimeDescriptor: null),
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "strong",
+ typeName: "StrongTagHelper",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: allowedChildren,
+ tagStructure: TagStructure.Unspecified,
+ designTimeDescriptor: null),
+ new TagHelperDescriptor(
+ prefix: string.Empty,
+ tagName: "br",
+ typeName: "BRTagHelper",
+ assemblyName: "SomeAssembly",
+ attributes: new TagHelperAttributeDescriptor[0],
+ requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
+ tagStructure: TagStructure.WithoutEndTag,
+ designTimeDescriptor: null),
+ };
+ var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
+
+ // Act & Assert
+ EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors);
+ }
+
[Fact]
public void Rewrite_CanHandleStartTagOnlyTagTagMode()
{
@@ -33,6 +477,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[0],
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null)
};
@@ -68,6 +513,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[0],
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null)
};
@@ -104,6 +550,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[0],
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null),
new TagHelperDescriptor(
@@ -113,6 +560,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[0],
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: TagStructure.NormalOrSelfClosing,
designTimeDescriptor: null)
};
@@ -1022,6 +1470,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null),
new TagHelperDescriptor(
@@ -1039,6 +1488,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
designTimeDescriptor: null),
},
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null)
};
@@ -1051,6 +1501,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null),
new TagHelperDescriptor(
@@ -1068,6 +1519,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
designTimeDescriptor: null),
},
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null)
};
@@ -1080,6 +1532,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty(),
requiredAttributes: Enumerable.Empty(),
+ allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null),
};