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
This commit is contained in:
N. Taylor Mullen 2015-08-13 16:15:54 -07:00
parent 3361507c29
commit 465ff9713d
19 changed files with 859 additions and 14 deletions

View File

@ -394,6 +394,38 @@ namespace Microsoft.AspNet.Razor.Runtime
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperAttributeList_CannotAddAttribute"), p0, p1, p2, p3);
}
/// <summary>
/// Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName
{
get { return GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName"); }
}
/// <summary>
/// Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character.
/// </summary>
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);
}
/// <summary>
/// Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace
{
get { return GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace"); }
}
/// <summary>
/// Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace.
/// </summary>
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);

View File

@ -189,4 +189,10 @@
<data name="TagHelperAttributeList_CannotAddAttribute" xml:space="preserve">
<value>Cannot add a {0} with inconsistent names. The {1} property '{2}' must match the location '{3}'.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName" xml:space="preserve">
<value>Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace" xml:space="preserve">
<value>Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace.</value>
</data>
</root>

View File

@ -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
{
/// <summary>
/// Restricts children of the <see cref="ITagHelper"/>'s element.
/// </summary>
/// <remarks>Combining this attribute with a <see cref="TargetElementAttribute"/> that specifies its
/// <see cref="TargetElementAttribute.TagStructure"/> as <see cref="TagStructure.WithoutEndTag"/> will result in
/// this attribute being ignored.</remarks>
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class RestrictChildrenAttribute : Attribute
{
/// <summary>
/// Instantiates a new instance of the <see cref="RestrictChildrenAttribute"/> class.
/// </summary>
/// <param name="tagName">
/// The tag name of an element allowed as a child. Tag helpers must target the element.
/// </param>
/// <param name="tagNames">
/// Additional names of elements allowed as children. Tag helpers must target all such elements.
/// </param>
public RestrictChildrenAttribute(string tagName, params string[] tagNames)
{
var concatenatedNames = new string[1 + tagNames.Length];
concatenatedNames[0] = tagName;
tagNames.CopyTo(concatenatedNames, 1);
ChildTagNames = concatenatedNames;
}
/// <summary>
/// Get the names of elements allowed as children. Tag helpers must target all such elements.
/// </summary>
public IEnumerable<string> ChildTagNames { get; }
}
}

View File

@ -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<TagHelperAttributeDescriptor> attributeDescriptors,
IEnumerable<TargetElementAttribute> targetElementAttributes,
IEnumerable<string> allowedChildren,
bool designTime)
{
TagHelperDesignTimeDescriptor typeDesignTimeDescriptor = null;
@ -118,6 +121,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName,
attributeDescriptors,
requiredAttributes: Enumerable.Empty<string>(),
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<string> GetAllowedChildren(TypeInfo typeInfo, ErrorSink errorSink)
{
var restrictChildrenAttribute = typeInfo.GetCustomAttribute<RestrictChildrenAttribute>(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<string> GetValidAllowedChildren(
IEnumerable<string> allowedChildren,
string tagHelperName,
ErrorSink errorSink)
{
var validAllowedChildren = new List<string>();
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<TagHelperAttributeDescriptor> attributeDescriptors,
TargetElementAttribute targetElementAttribute,
IEnumerable<string> 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<TagHelperAttributeDescriptor> attributeDescriptors,
IEnumerable<string> requiredAttributes,
IEnumerable<string> 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<char, string> 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;
}

View File

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

View File

@ -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));

View File

@ -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<TagHelperBlockTracker> _trackerStack;
private TagHelperBlockTracker _currentTagHelperTracker;
private Stack<BlockBuilder> _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<TagHelperDescriptor> 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<string> AllowedChildren { get; }
}
}
}

View File

@ -1514,6 +1514,38 @@ namespace Microsoft.AspNet.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag"), p0, p1, p2);
}
/// <summary>
/// The parent &amp;lt;{0}&amp;gt; tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed.
/// </summary>
internal static string TagHelperParseTreeRewriter_CannotHaveNonTagContent
{
get { return GetString("TagHelperParseTreeRewriter_CannotHaveNonTagContent"); }
}
/// <summary>
/// The parent &amp;lt;{0}&amp;gt; tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed.
/// </summary>
internal static string FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_CannotHaveNonTagContent"), p0, p1);
}
/// <summary>
/// The &amp;lt;{0}&amp;gt; tag is not allowed by parent &amp;lt;{1}&amp;gt; tag helper. Only child tag helper(s) targeting tag name(s) '{2}' are allowed.
/// </summary>
internal static string TagHelperParseTreeRewriter_InvalidNestedTag
{
get { return GetString("TagHelperParseTreeRewriter_InvalidNestedTag"); }
}
/// <summary>
/// The &amp;lt;{0}&amp;gt; tag is not allowed by parent &amp;lt;{1}&amp;gt; tag helper. Only child tag helper(s) targeting tag name(s) '{2}' are allowed.
/// </summary>
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);

View File

@ -419,4 +419,10 @@ Instead, wrap the contents of the block in "{{}}":
<data name="TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag" xml:space="preserve">
<value>Found an end tag (&amp;lt;/{0}&amp;gt;) for tag helper '{1}' with tag structure that disallows an end tag ('{2}').</value>
</data>
<data name="TagHelperParseTreeRewriter_CannotHaveNonTagContent" xml:space="preserve">
<value>The parent &amp;lt;{0}&amp;gt; tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed.</value>
</data>
<data name="TagHelperParseTreeRewriter_InvalidNestedTag" xml:space="preserve">
<value>The &amp;lt;{0}&amp;gt; tag is not allowed by parent &amp;lt;{1}&amp;gt; tag helper. Only child tag helper(s) targeting tag name(s) '{2}' are allowed.</value>
</data>
</root>

View File

@ -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
/// <param name="requiredAttributes">
/// The attribute names required for the tag helper to target the HTML tag.
/// </param>
/// <param name="allowedChildren">
/// The names of elements allowed as children. Tag helpers must target all such elements.
/// </param>
/// <param name="tagStructure">The expected tag structure.</param>
/// <param name="designTimeDescriptor">The <see cref="TagHelperDesignTimeDescriptor"/> that contains design
/// time information about the tag helper.</param>
public TagHelperDescriptor(
@ -93,6 +98,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
[NotNull] string assemblyName,
[NotNull] IEnumerable<TagHelperAttributeDescriptor> attributes,
[NotNull] IEnumerable<string> requiredAttributes,
IEnumerable<string> allowedChildren,
TagStructure tagStructure,
TagHelperDesignTimeDescriptor designTimeDescriptor)
{
@ -105,6 +111,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers
RequiredAttributes = new List<string>(requiredAttributes);
TagStructure = tagStructure;
DesignTimeDescriptor = designTimeDescriptor;
if (allowedChildren != null)
{
AllowedChildren = new List<string>(allowedChildren);
}
}
/// <summary>
@ -147,6 +158,12 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// </remarks>
public IReadOnlyList<string> RequiredAttributes { get; }
/// <summary>
/// Get the names of elements allowed as children. Tag helpers must target all such elements.
/// </summary>
/// <remarks><c>null</c> indicates all children are allowed.</remarks>
public IReadOnlyList<string> AllowedChildren { get; }
/// <summary>
/// The expected tag structure.
/// </summary>

View File

@ -30,7 +30,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <remarks>
/// Determines equality based on <see cref="TagHelperDescriptor.TypeName"/>,
/// <see cref="TagHelperDescriptor.AssemblyName"/>, <see cref="TagHelperDescriptor.TagName"/>,
/// <see cref="TagHelperDescriptor.RequiredAttributes"/>, and <see cref="TagHelperDescriptor.TagStructure"/>.
/// <see cref="TagHelperDescriptor.RequiredAttributes"/>, <see cref="TagHelperDescriptor.AllowedChildren"/>,
/// and <see cref="TagHelperDescriptor.TagStructure"/>.
/// Ignores <see cref="TagHelperDescriptor.DesignTimeDescriptor"/> because it can be inferred directly from
/// <see cref="TagHelperDescriptor.TypeName"/> and <see cref="TagHelperDescriptor.AssemblyName"/>.
/// </remarks>
@ -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;
}
}

View File

@ -35,6 +35,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null)
}
@ -50,6 +51,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null),
new TagHelperDescriptor(
@ -59,6 +61,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: TagStructure.NormalOrSelfClosing,
designTimeDescriptor: null),
}
@ -74,6 +77,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: TagStructure.WithoutEndTag,
designTimeDescriptor: null),
new TagHelperDescriptor(
@ -83,6 +87,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: TagStructure.Unspecified,
designTimeDescriptor: null),
}
@ -137,6 +142,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: new TagHelperDesignTimeDescriptor
{
@ -155,6 +161,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: new TagHelperDesignTimeDescriptor
{
@ -167,6 +174,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: new TagHelperDesignTimeDescriptor
{
@ -1734,6 +1742,42 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
}
}
public static TheoryData<string, string[]> 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<string, string[]> GetInvalidNameOrPrefixData(
Func<string, string, string> onNameError,
string whitespaceErrorString,
@ -1973,6 +2017,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: assemblyName,
attributes: attributes ?? Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: requiredAttributes ?? Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
}

View File

@ -32,6 +32,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
}
@ -48,6 +49,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: AssemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
}
@ -609,6 +611,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: assemblyB,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
@ -1045,6 +1048,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName: assemblyB,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
@ -1444,6 +1448,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
assemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
}

View File

@ -1550,6 +1550,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
new TagHelperAttributeDescriptor("age", pAgePropertyInfo)
},
requiredAttributes: Enumerable.Empty<string>(),
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<string>(),
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<string>(),
allowedChildren: null,
tagStructure: TagStructure.Unspecified,
designTimeDescriptor: null)
};

View File

@ -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(

View File

@ -87,6 +87,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[0],
requiredAttributes: Enumerable.Empty<string>(),
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<string>(),
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<string>(),
allowedChildren: null,
tagStructure: structure2,
designTimeDescriptor: null)
};

View File

@ -322,6 +322,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null);
}

View File

@ -23,6 +23,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
assemblyName: "assembly name",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
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<string>(),
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<string>(),
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<TagHelperAttributeDescriptor>(),
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<string>(),
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<string>(),
allowedChildren: null,
tagStructure: TagStructure.NormalOrSelfClosing,
designTimeDescriptor: null);

View File

@ -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 = $"<p>{Environment.NewLine} <strong>{Environment.NewLine} Hello" +
$"{Environment.NewLine} </strong>{Environment.NewLine}</p>";
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("<strong>"),
factory.Markup(Environment.NewLine + " Hello" + Environment.NewLine + " "),
blockFactory.MarkupTagBlock("</strong>"),
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<string>(),
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 = "<strong required><strong></strong></strong>";
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<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>("required", null)
},
blockFactory.MarkupTagBlock("<strong>"),
blockFactory.MarkupTagBlock("</strong>")));
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 = "<p><strong>Hello World</strong><br></p>";
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<string>(),
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<string>(),
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<string>(),
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<string>(),
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 = "<p><strong>Hello World</strong><br></p>";
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<string>(),
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<string>(),
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<string>(),
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<string>(),
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<string, string, string, int, int, RazorError> nestedTagError =
(childName, parentName, allowed, location, length) => new RazorError(
RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag(
childName,
parentName,
allowed),
absoluteIndex: location,
lineIndex: 0,
columnIndex: location,
length: length);
Func<string, string, int, int, RazorError> nestedContentError =
(parentName, allowed, location, length) => new RazorError(
RazorResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent(parentName, allowed),
absoluteIndex: location,
lineIndex: 0,
columnIndex: location,
length: length);
return new TheoryData<string, IEnumerable<string>, MarkupBlock, RazorError[]>
{
{
"<p><br /></p>",
new[] { "br" },
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("br", TagMode.SelfClosing))),
new RazorError[0]
},
{
$"<p>{Environment.NewLine}<br />{Environment.NewLine}</p>",
new[] { "br" },
new MarkupBlock(
new MarkupTagHelperBlock("p",
factory.Markup(Environment.NewLine),
new MarkupTagHelperBlock("br", TagMode.SelfClosing),
factory.Markup(Environment.NewLine))),
new RazorError[0]
},
{
"<p><br></p>",
new[] { "strong" },
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("br", TagMode.StartTagOnly))),
new[] { nestedTagError("br", "p", "strong", 3, 4) }
},
{
"<p>Hello</p>",
new[] { "strong" },
new MarkupBlock(new MarkupTagHelperBlock("p", factory.Markup("Hello"))),
new[] { nestedContentError("p", "strong", 3, 5) }
},
{
"<p><hr /></p>",
new[] { "br", "strong" },
new MarkupBlock(new MarkupTagHelperBlock("p", blockFactory.MarkupTagBlock("<hr />"))),
new[] { nestedTagError("hr", "p", "br, strong", 3, 6) }
},
{
"<p><br>Hello</p>",
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) }
},
{
"<p><strong>Title:</strong><br />Something</p>",
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),
}
},
{
"<p><strong>Title:</strong><br />Something</p>",
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),
}
},
{
"<p> <strong>Title:</strong> <br /> Something</p>",
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),
}
},
{
"<p><strong>Title:<br><em>A Very Cool</em></strong><br />Something</p>",
new[] { "strong" },
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("strong",
factory.Markup("Title:"),
new MarkupTagHelperBlock("br", TagMode.StartTagOnly),
blockFactory.MarkupTagBlock("<em>"),
factory.Markup("A Very Cool"),
blockFactory.MarkupTagBlock("</em>")),
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)
}
},
{
"<p><custom>Title:<br><em>A Very Cool</em></custom><br />Something</p>",
new[] { "custom" },
new MarkupBlock(
new MarkupTagHelperBlock("p",
blockFactory.MarkupTagBlock("<custom>"),
factory.Markup("Title:"),
new MarkupTagHelperBlock("br", TagMode.StartTagOnly),
blockFactory.MarkupTagBlock("<em>"),
factory.Markup("A Very Cool"),
blockFactory.MarkupTagBlock("</em>"),
blockFactory.MarkupTagBlock("</custom>"),
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<string> 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<string>(),
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<string>(),
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<string>(),
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<string>(),
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<string>(),
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<string>(),
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<string>(),
allowedChildren: null,
tagStructure: TagStructure.NormalOrSelfClosing,
designTimeDescriptor: null)
};
@ -1022,6 +1470,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null),
new TagHelperDescriptor(
@ -1039,6 +1488,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
designTimeDescriptor: null),
},
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null)
};
@ -1051,6 +1501,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null),
new TagHelperDescriptor(
@ -1068,6 +1519,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
designTimeDescriptor: null),
},
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null)
};
@ -1080,6 +1532,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
assemblyName: "SomeAssembly",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: Enumerable.Empty<string>(),
allowedChildren: null,
tagStructure: default(TagStructure),
designTimeDescriptor: null),
};