Port existing TagHelper infrastructure.

- Modified how tests are run to reflect our new test infrastructure.
- Added TagHelper assertion bits.
- Moved all classes to the Evolution.Legacy namespace.
- Copied Test.Sources bits to the Evolution.Test project.

#845
This commit is contained in:
N. Taylor Mullen 2016-11-14 15:08:43 -08:00
parent 51fb0c993b
commit 26a1cf3cff
45 changed files with 14382 additions and 1 deletions

View File

@ -82,6 +82,26 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
}
public Span FindFirstDescendentSpan()
{
SyntaxTreeNode current = this;
while (current != null && current.IsBlock)
{
current = ((Block)current).Children.FirstOrDefault();
}
return current as Span;
}
public Span FindLastDescendentSpan()
{
SyntaxTreeNode current = this;
while (current != null && current.IsBlock)
{
current = ((Block)current).Children.LastOrDefault();
}
return current as Span;
}
public override string ToString()
{
return string.Format(

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
/// <summary>
/// <see cref="RazorError"/>s collected.
/// </summary>
public IEnumerable<RazorError> Errors => _errors;
public IReadOnlyList<RazorError> Errors => _errors;
/// <summary>
/// Tracks the given <paramref name="error"/>.

View File

@ -0,0 +1,13 @@
// 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.
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal enum HtmlAttributeValueStyle
{
DoubleQuotes,
SingleQuotes,
NoQuotes,
Minimized,
}
}

View File

@ -0,0 +1,23 @@
// 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.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// Contract used to resolve <see cref="TagHelperDescriptor"/>s.
/// </summary>
internal interface ITagHelperDescriptorResolver
{
/// <summary>
/// Resolves <see cref="TagHelperDescriptor"/>s based on the given <paramref name="resolutionContext"/>.
/// </summary>
/// <param name="resolutionContext">
/// <see cref="TagHelperDescriptorResolutionContext"/> used to resolve descriptors for the Razor page.
/// </param>
/// <returns>An <see cref="IEnumerable{TagHelperDescriptor}"/> of <see cref="TagHelperDescriptor"/>s based
/// on the given <paramref name="resolutionContext"/>.</returns>
IEnumerable<TagHelperDescriptor> Resolve(TagHelperDescriptorResolutionContext resolutionContext);
}
}

View File

@ -0,0 +1,156 @@
// 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.Reflection;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// A metadata class describing a tag helper attribute.
/// </summary>
internal class TagHelperAttributeDescriptor
{
private string _typeName;
private string _name;
private string _propertyName;
/// <summary>
/// Instantiates a new instance of the <see cref="TagHelperAttributeDescriptor"/> class.
/// </summary>
public TagHelperAttributeDescriptor()
{
}
// Internal for testing i.e. for easy TagHelperAttributeDescriptor creation when PropertyInfo is available.
internal TagHelperAttributeDescriptor(string name, PropertyInfo propertyInfo)
{
Name = name;
PropertyName = propertyInfo.Name;
TypeName = propertyInfo.PropertyType.FullName;
IsEnum = propertyInfo.PropertyType.GetTypeInfo().IsEnum;
}
/// <summary>
/// Gets an indication whether this <see cref="TagHelperAttributeDescriptor"/> is used for dictionary indexer
/// assignments.
/// </summary>
/// <value>
/// If <c>true</c> this <see cref="TagHelperAttributeDescriptor"/> should be associated with all HTML
/// attributes that have names starting with <see cref="Name"/>. Otherwise this
/// <see cref="TagHelperAttributeDescriptor"/> is used for property assignment and is only associated with an
/// HTML attribute that has the exact <see cref="Name"/>.
/// </value>
/// <remarks>
/// HTML attribute names are matched case-insensitively, regardless of <see cref="IsIndexer"/>.
/// </remarks>
public bool IsIndexer { get; set; }
/// <summary>
/// Gets or sets an indication whether this property is an <see cref="Enum"/>.
/// </summary>
public bool IsEnum { get; set; }
/// <summary>
/// Gets or sets an indication whether this property is of type <see cref="string"/> or, if
/// <see cref="IsIndexer"/> is <c>true</c>, whether the indexer's value is of type <see cref="string"/>.
/// </summary>
/// <value>
/// If <c>true</c> the <see cref="TypeName"/> is for <see cref="string"/>. This causes the Razor parser
/// to allow empty values for HTML attributes matching this <see cref="TagHelperAttributeDescriptor"/>. If
/// <c>false</c> empty values for such matching attributes lead to errors.
/// </value>
public bool IsStringProperty { get; set; }
/// <summary>
/// The HTML attribute name or, if <see cref="IsIndexer"/> is <c>true</c>, the prefix for matching attribute
/// names.
/// </summary>
public string Name
{
get
{
return _name;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_name = value;
}
}
/// <summary>
/// The name of the CLR property that corresponds to the HTML attribute.
/// </summary>
public string PropertyName
{
get
{
return _propertyName;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_propertyName = value;
}
}
/// <summary>
/// The full name of the named (see <see name="PropertyName"/>) property's <see cref="Type"/> or, if
/// <see cref="IsIndexer"/> is <c>true</c>, the full name of the indexer's value <see cref="Type"/>.
/// </summary>
public string TypeName
{
get
{
return _typeName;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_typeName = value;
IsStringProperty = string.Equals(TypeName, typeof(string).FullName, StringComparison.Ordinal);
}
}
/// <summary>
/// The <see cref="TagHelperAttributeDesignTimeDescriptor"/> that contains design time information about
/// this attribute.
/// </summary>
public TagHelperAttributeDesignTimeDescriptor DesignTimeDescriptor { get; set; }
/// <summary>
/// Determines whether HTML attribute <paramref name="name"/> matches this
/// <see cref="TagHelperAttributeDescriptor"/>.
/// </summary>
/// <param name="name">Name of the HTML attribute to check.</param>
/// <returns>
/// <c>true</c> if this <see cref="TagHelperAttributeDescriptor"/> matches <paramref name="name"/>.
/// <c>false</c> otherwise.
/// </returns>
public bool IsNameMatch(string name)
{
if (IsIndexer)
{
return name.StartsWith(Name, StringComparison.OrdinalIgnoreCase);
}
else
{
return string.Equals(name, Name, StringComparison.OrdinalIgnoreCase);
}
}
}
}

View File

@ -0,0 +1,21 @@
// 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.
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// A metadata class containing information about tag helper use.
/// </summary>
internal class TagHelperAttributeDesignTimeDescriptor
{
/// <summary>
/// A summary of how to use a tag helper.
/// </summary>
public string Summary { get; set; }
/// <summary>
/// Remarks about how to use a tag helper.
/// </summary>
public string Remarks { get; set; }
}
}

View File

@ -0,0 +1,27 @@
// 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.
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal class TagHelperAttributeNode
{
public TagHelperAttributeNode(string name, SyntaxTreeNode value, HtmlAttributeValueStyle valueStyle)
{
Name = name;
Value = value;
ValueStyle = valueStyle;
}
// Internal for testing
internal TagHelperAttributeNode(string name, SyntaxTreeNode value)
: this(name, value, HtmlAttributeValueStyle.DoubleQuotes)
{
}
public string Name { get; }
public SyntaxTreeNode Value { get; }
public HtmlAttributeValueStyle ValueStyle { get; }
}
}

View File

@ -0,0 +1,159 @@
// 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;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// A <see cref="Block"/> that reprents a special HTML element.
/// </summary>
internal class TagHelperBlock : Block, IEquatable<TagHelperBlock>
{
private readonly SourceLocation _start;
/// <summary>
/// Instantiates a new instance of a <see cref="TagHelperBlock"/>.
/// </summary>
/// <param name="source">A <see cref="TagHelperBlockBuilder"/> used to construct a valid
/// <see cref="TagHelperBlock"/>.</param>
public TagHelperBlock(TagHelperBlockBuilder source)
: base(source.Type, source.Children, source.ChunkGenerator)
{
TagName = source.TagName;
Descriptors = source.Descriptors;
Attributes = new List<TagHelperAttributeNode>(source.Attributes);
_start = source.Start;
TagMode = source.TagMode;
SourceStartTag = source.SourceStartTag;
SourceEndTag = source.SourceEndTag;
source.Reset();
foreach (var attributeChildren in Attributes)
{
if (attributeChildren.Value != null)
{
attributeChildren.Value.Parent = this;
}
}
}
/// <summary>
/// Gets the unrewritten source start tag.
/// </summary>
/// <remarks>This is used by design time to properly format <see cref="TagHelperBlock"/>s.</remarks>
public Block SourceStartTag { get; }
/// <summary>
/// Gets the unrewritten source end tag.
/// </summary>
/// <remarks>This is used by design time to properly format <see cref="TagHelperBlock"/>s.</remarks>
public Block SourceEndTag { get; }
/// <summary>
/// Gets the HTML syntax of the element in the Razor source.
/// </summary>
public TagMode TagMode { get; }
/// <summary>
/// <see cref="TagHelperDescriptor"/>s for the HTML element.
/// </summary>
public IEnumerable<TagHelperDescriptor> Descriptors { get; }
/// <summary>
/// The HTML attributes.
/// </summary>
public IList<TagHelperAttributeNode> Attributes { get; }
/// <inheritdoc />
public override SourceLocation Start
{
get
{
return _start;
}
}
/// <summary>
/// The HTML tag name.
/// </summary>
public string TagName { get; }
public override int Length
{
get
{
var startTagLength = SourceStartTag?.Length ?? 0;
var childrenLength = base.Length;
var endTagLength = SourceEndTag?.Length ?? 0;
return startTagLength + childrenLength + endTagLength;
}
}
public override IEnumerable<Span> Flatten()
{
if (SourceStartTag != null)
{
foreach (var childSpan in SourceStartTag.Flatten())
{
yield return childSpan;
}
}
foreach (var childSpan in base.Flatten())
{
yield return childSpan;
}
if (SourceEndTag != null)
{
foreach (var childSpan in SourceEndTag.Flatten())
{
yield return childSpan;
}
}
}
/// <inheritdoc />
public override string ToString()
{
return string.Format(CultureInfo.CurrentCulture,
"'{0}' (Attrs: {1}) Tag Helper Block at {2}::{3} (Gen:{4})",
TagName, Attributes.Count, Start, Length, ChunkGenerator);
}
/// <summary>
/// Determines whether two <see cref="TagHelperBlock"/>s are equal by comparing the <see cref="TagName"/>,
/// <see cref="Attributes"/>, <see cref="Block.Type"/>, <see cref="Block.ChunkGenerator"/> and
/// <see cref="Block.Children"/>.
/// </summary>
/// <param name="other">The <see cref="TagHelperBlock"/> to check equality against.</param>
/// <returns>
/// <c>true</c> if the current <see cref="TagHelperBlock"/> is equivalent to the given
/// <paramref name="other"/>, <c>false</c> otherwise.
/// </returns>
public bool Equals(TagHelperBlock other)
{
return base.Equals(other) &&
string.Equals(TagName, other.TagName, StringComparison.OrdinalIgnoreCase) &&
Attributes.SequenceEqual(other.Attributes);
}
/// <inheritdoc />
public override int GetHashCode()
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(base.GetHashCode());
hashCodeCombiner.Add(TagName, StringComparer.OrdinalIgnoreCase);
hashCodeCombiner.Add(Attributes);
return hashCodeCombiner;
}
}
}

View File

@ -0,0 +1,134 @@
// 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.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// A <see cref="BlockBuilder"/> used to create <see cref="TagHelperBlock"/>s.
/// </summary>
internal class TagHelperBlockBuilder : BlockBuilder
{
/// <summary>
/// Instantiates a new <see cref="TagHelperBlockBuilder"/> instance based on the given
/// <paramref name="original"/>.
/// </summary>
/// <param name="original">The original <see cref="TagHelperBlock"/> to copy data from.</param>
public TagHelperBlockBuilder(TagHelperBlock original)
: base(original)
{
TagName = original.TagName;
Descriptors = original.Descriptors;
Attributes = new List<TagHelperAttributeNode>(original.Attributes);
}
/// <summary>
/// Instantiates a new instance of the <see cref="TagHelperBlockBuilder"/> class
/// with the provided values.
/// </summary>
/// <param name="tagName">An HTML tag name.</param>
/// <param name="tagMode">HTML syntax of the element in the Razor source.</param>
/// <param name="start">Starting location of the <see cref="TagHelperBlock"/>.</param>
/// <param name="attributes">Attributes of the <see cref="TagHelperBlock"/>.</param>
/// <param name="descriptors">The <see cref="TagHelperDescriptor"/>s associated with the current HTML
/// tag.</param>
public TagHelperBlockBuilder(
string tagName,
TagMode tagMode,
SourceLocation start,
IList<TagHelperAttributeNode> attributes,
IEnumerable<TagHelperDescriptor> descriptors)
{
TagName = tagName;
TagMode = tagMode;
Start = start;
Descriptors = descriptors;
Attributes = new List<TagHelperAttributeNode>(attributes);
Type = BlockType.Tag;
ChunkGenerator = new TagHelperChunkGenerator(descriptors);
}
// Internal for testing
internal TagHelperBlockBuilder(
string tagName,
TagMode tagMode,
IList<TagHelperAttributeNode> attributes,
IEnumerable<SyntaxTreeNode> children)
{
TagName = tagName;
TagMode = tagMode;
Attributes = attributes;
Type = BlockType.Tag;
ChunkGenerator = new TagHelperChunkGenerator(tagHelperDescriptors: null);
// Children is IList, no AddRange
foreach (var child in children)
{
Children.Add(child);
}
}
/// <summary>
/// Gets or sets the unrewritten source start tag.
/// </summary>
/// <remarks>This is used by design time to properly format <see cref="TagHelperBlock"/>s.</remarks>
public Block SourceStartTag { get; set; }
/// <summary>
/// Gets or sets the unrewritten source end tag.
/// </summary>
/// <remarks>This is used by design time to properly format <see cref="TagHelperBlock"/>s.</remarks>
public Block SourceEndTag { get; set; }
/// <summary>
/// Gets the HTML syntax of the element in the Razor source.
/// </summary>
public TagMode TagMode { get; }
/// <summary>
/// <see cref="TagHelperDescriptor"/>s for the HTML element.
/// </summary>
public IEnumerable<TagHelperDescriptor> Descriptors { get; }
/// <summary>
/// The HTML attributes.
/// </summary>
public IList<TagHelperAttributeNode> Attributes { get; }
/// <summary>
/// The HTML tag name.
/// </summary>
public string TagName { get; set; }
/// <summary>
/// Constructs a new <see cref="TagHelperBlock"/>.
/// </summary>
/// <returns>A <see cref="TagHelperBlock"/>.</returns>
public override Block Build()
{
return new TagHelperBlock(this);
}
/// <inheritdoc />
/// <remarks>
/// Sets the <see cref="TagName"/> to <c>null</c> and clears the <see cref="Attributes"/>.
/// </remarks>
public override void Reset()
{
TagName = null;
if (Attributes != null)
{
Attributes.Clear();
}
base.Reset();
}
/// <summary>
/// The starting <see cref="SourceLocation"/> of the tag helper.
/// </summary>
public SourceLocation Start { get; set; }
}
}

View File

@ -0,0 +1,735 @@
// 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;
using System.Diagnostics;
using System.Linq;
using System.Text;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal static class TagHelperBlockRewriter
{
private static readonly string StringTypeName = typeof(string).FullName;
public static TagHelperBlockBuilder Rewrite(
string tagName,
bool validStructure,
Block tag,
IEnumerable<TagHelperDescriptor> descriptors,
ErrorSink errorSink)
{
// There will always be at least one child for the '<'.
var start = tag.Children.First().Start;
var attributes = GetTagAttributes(tagName, validStructure, tag, descriptors, errorSink);
var tagMode = GetTagMode(tagName, tag, descriptors, errorSink);
return new TagHelperBlockBuilder(tagName, tagMode, start, attributes, descriptors);
}
private static IList<TagHelperAttributeNode> GetTagAttributes(
string tagName,
bool validStructure,
Block tagBlock,
IEnumerable<TagHelperDescriptor> descriptors,
ErrorSink errorSink)
{
// Ignore all but one descriptor per type since this method uses the TagHelperDescriptors only to get the
// contained TagHelperAttributeDescriptor's.
descriptors = descriptors.Distinct(TypeBasedTagHelperDescriptorComparer.Default);
var attributes = new List<TagHelperAttributeNode>();
// We skip the first child "<tagname" and take everything up to the ending portion of the tag ">" or "/>".
// The -2 accounts for both the start and end tags. If the tag does not have a valid structure then there's
// no end tag to ignore.
var symbolOffset = validStructure ? 2 : 1;
var attributeChildren = tagBlock.Children.Skip(1).Take(tagBlock.Children.Count() - symbolOffset);
foreach (var child in attributeChildren)
{
TryParseResult result;
if (child.IsBlock)
{
result = TryParseBlock(tagName, (Block)child, descriptors, errorSink);
}
else
{
result = TryParseSpan((Span)child, descriptors, errorSink);
}
// Only want to track the attribute if we succeeded in parsing its corresponding Block/Span.
if (result != null)
{
SourceLocation? errorLocation = null;
// Check if it's a bound attribute that is minimized or if it's a bound non-string attribute that
// is null or whitespace.
if ((result.IsBoundAttribute && result.AttributeValueNode == null) ||
(result.IsBoundNonStringAttribute &&
IsNullOrWhitespaceAttributeValue(result.AttributeValueNode)))
{
errorLocation = GetAttributeNameStartLocation(child);
errorSink.OnError(
errorLocation.Value,
LegacyResources.FormatRewriterError_EmptyTagHelperBoundAttribute(
result.AttributeName,
tagName,
GetPropertyType(result.AttributeName, descriptors)),
result.AttributeName.Length);
}
// Check if the attribute was a prefix match for a tag helper dictionary property but the
// dictionary key would be the empty string.
if (result.IsMissingDictionaryKey)
{
if (!errorLocation.HasValue)
{
errorLocation = GetAttributeNameStartLocation(child);
}
errorSink.OnError(
errorLocation.Value,
LegacyResources.FormatTagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey(
result.AttributeName,
tagName),
result.AttributeName.Length);
}
var attributeNode = new TagHelperAttributeNode(
result.AttributeName,
result.AttributeValueNode,
result.AttributeValueStyle);
attributes.Add(attributeNode);
}
else
{
// Error occured while parsing the attribute. Don't try parsing the rest to avoid misleading errors.
break;
}
}
return attributes;
}
private static TagMode GetTagMode(
string tagName,
Block beginTagBlock,
IEnumerable<TagHelperDescriptor> descriptors,
ErrorSink errorSink)
{
var childSpan = beginTagBlock.FindLastDescendentSpan();
// Self-closing tags are always valid despite descriptors[X].TagStructure.
if (childSpan?.Content.EndsWith("/>", StringComparison.Ordinal) ?? false)
{
return TagMode.SelfClosing;
}
var baseDescriptor = descriptors.FirstOrDefault(
descriptor => descriptor.TagStructure != TagStructure.Unspecified);
var resolvedTagStructure = baseDescriptor?.TagStructure ?? TagStructure.Unspecified;
if (resolvedTagStructure == TagStructure.WithoutEndTag)
{
return TagMode.StartTagOnly;
}
return TagMode.StartTagAndEndTag;
}
// This method handles cases when the attribute is a simple span attribute such as
// class="something moresomething". This does not handle complex attributes such as
// class="@myclass". Therefore the span.Content is equivalent to the entire attribute.
private static TryParseResult TryParseSpan(
Span span,
IEnumerable<TagHelperDescriptor> descriptors,
ErrorSink errorSink)
{
var afterEquals = false;
var builder = new SpanBuilder
{
ChunkGenerator = span.ChunkGenerator,
EditHandler = span.EditHandler,
Kind = span.Kind
};
// Will contain symbols that represent a single attribute value: <input| class="btn"| />
var htmlSymbols = span.Symbols.OfType<HtmlSymbol>().ToArray();
var capturedAttributeValueStart = false;
var attributeValueStartLocation = span.Start;
// Default to DoubleQuotes. We purposefully do not persist NoQuotes ValueStyle to stay consistent with the
// TryParseBlock() variation of attribute parsing.
var attributeValueStyle = HtmlAttributeValueStyle.DoubleQuotes;
// The symbolOffset is initialized to 0 to expect worst case: "class=". If a quote is found later on for
// the attribute value the symbolOffset is adjusted accordingly.
var symbolOffset = 0;
string name = null;
// Iterate down through the symbols to find the name and the start of the value.
// We subtract the symbolOffset so we don't accept an ending quote of a span.
for (var i = 0; i < htmlSymbols.Length - symbolOffset; i++)
{
var symbol = htmlSymbols[i];
if (afterEquals)
{
// We've captured all leading whitespace, the attribute name, and an equals with an optional
// quote/double quote. We're now at: " asp-for='|...'" or " asp-for=|..."
// The goal here is to capture all symbols until the end of the attribute. Note this will not
// consume an ending quote due to the symbolOffset.
// When symbols are accepted into SpanBuilders, their locations get altered to be offset by the
// parent which is why we need to mark our start location prior to adding the symbol.
// This is needed to know the location of the attribute value start within the document.
if (!capturedAttributeValueStart)
{
capturedAttributeValueStart = true;
attributeValueStartLocation = span.Start + symbol.Start;
}
builder.Accept(symbol);
}
else if (name == null && HtmlMarkupParser.IsValidAttributeNameSymbol(symbol))
{
// We've captured all leading whitespace prior to the attribute name.
// We're now at: " |asp-for='...'" or " |asp-for=..."
// The goal here is to capture the attribute name.
var nameBuilder = new StringBuilder();
// Move the indexer past the attribute name symbols.
for (var j = i; j < htmlSymbols.Length; j++)
{
var nameSymbol = htmlSymbols[j];
if (!HtmlMarkupParser.IsValidAttributeNameSymbol(nameSymbol))
{
break;
}
nameBuilder.Append(nameSymbol.Content);
i++;
}
i--;
name = nameBuilder.ToString();
attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, name);
}
else if (symbol.Type == HtmlSymbolType.Equals)
{
// We've captured all leading whitespace and the attribute name.
// We're now at: " asp-for|='...'" or " asp-for|=..."
// The goal here is to consume the equal sign and the optional single/double-quote.
// The coming symbols will either be a quote or value (in the case that the value is unquoted).
SourceLocation symbolStartLocation;
// Skip the whitespace preceding the start of the attribute value.
do
{
i++; // Start from the symbol after '='.
} while (i < htmlSymbols.Length &&
(htmlSymbols[i].Type == HtmlSymbolType.WhiteSpace ||
htmlSymbols[i].Type == HtmlSymbolType.NewLine));
// Check for attribute start values, aka single or double quote
if (i < htmlSymbols.Length && IsQuote(htmlSymbols[i]))
{
if (htmlSymbols[i].Type == HtmlSymbolType.SingleQuote)
{
attributeValueStyle = HtmlAttributeValueStyle.SingleQuotes;
}
symbolStartLocation = htmlSymbols[i].Start;
// If there's a start quote then there must be an end quote to be valid, skip it.
symbolOffset = 1;
}
else
{
// We are at the symbol after equals. Go back to equals to ensure we don't skip past that symbol.
i--;
symbolStartLocation = symbol.Start;
}
attributeValueStartLocation =
span.Start +
symbolStartLocation +
new SourceLocation(absoluteIndex: 1, lineIndex: 0, characterIndex: 1);
afterEquals = true;
}
else if (symbol.Type == HtmlSymbolType.WhiteSpace)
{
// We're at the start of the attribute, this branch may be hit on the first iterations of
// the loop since the parser separates attributes with their spaces included as symbols.
// We're at: "| asp-for='...'" or "| asp-for=..."
// Note: This will not be hit even for situations like asp-for ="..." because the core Razor
// parser currently does not know how to handle attributes in that format. This will be addressed
// by https://github.com/aspnet/Razor/issues/123.
attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, symbol.Content);
}
}
// After all symbols have been added we need to set the builders start position so we do not indirectly
// modify each symbol's Start location.
builder.Start = attributeValueStartLocation;
if (name == null)
{
// We couldn't find a name, if the original span content was whitespace it ultimately means the tag
// that owns this "attribute" is malformed and is expecting a user to type a new attribute.
// ex: <myTH class="btn"| |
if (!string.IsNullOrWhiteSpace(span.Content))
{
errorSink.OnError(
span.Start,
LegacyResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed,
span.Content.Length);
}
return null;
}
var result = CreateTryParseResult(name, descriptors);
// If we're not after an equal then we should treat the value as if it were a minimized attribute.
Span attributeValue = null;
if (afterEquals)
{
attributeValue = CreateMarkupAttribute(builder, result.IsBoundNonStringAttribute);
}
else
{
attributeValueStyle = HtmlAttributeValueStyle.Minimized;
}
result.AttributeValueNode = attributeValue;
result.AttributeValueStyle = attributeValueStyle;
return result;
}
private static TryParseResult TryParseBlock(
string tagName,
Block block,
IEnumerable<TagHelperDescriptor> descriptors,
ErrorSink errorSink)
{
// TODO: Accept more than just spans: https://github.com/aspnet/Razor/issues/96.
// The first child will only ever NOT be a Span if a user is doing something like:
// <input @checked />
var childSpan = block.Children.First() as Span;
if (childSpan == null || childSpan.Kind != SpanKind.Markup)
{
errorSink.OnError(
block.Start,
LegacyResources.FormatTagHelpers_CannotHaveCSharpInTagDeclaration(tagName),
block.Length);
return null;
}
var builder = new BlockBuilder(block);
// If there's only 1 child it means that it's plain text inside of the attribute.
// i.e. <div class="plain text in attribute">
if (builder.Children.Count == 1)
{
return TryParseSpan(childSpan, descriptors, errorSink);
}
var nameSymbols = childSpan
.Symbols
.OfType<HtmlSymbol>()
.SkipWhile(symbol => !HtmlMarkupParser.IsValidAttributeNameSymbol(symbol)) // Skip prefix
.TakeWhile(nameSymbol => HtmlMarkupParser.IsValidAttributeNameSymbol(nameSymbol))
.Select(nameSymbol => nameSymbol.Content);
var name = string.Concat(nameSymbols);
if (string.IsNullOrEmpty(name))
{
errorSink.OnError(
childSpan.Start,
LegacyResources.FormatTagHelpers_AttributesMustHaveAName(tagName),
childSpan.Length);
return null;
}
// Have a name now. Able to determine correct isBoundNonStringAttribute value.
var result = CreateTryParseResult(name, descriptors);
var firstChild = builder.Children[0] as Span;
if (firstChild != null && firstChild.Symbols[0] is HtmlSymbol)
{
var htmlSymbol = firstChild.Symbols[firstChild.Symbols.Count - 1] as HtmlSymbol;
switch (htmlSymbol.Type)
{
// Treat NoQuotes and DoubleQuotes equivalently. We purposefully do not persist NoQuotes
// ValueStyles at code generation time to protect users from rendering dynamic content with spaces
// that can break attributes.
// Ex: <tag my-attribute=@value /> where @value results in the test "hello world".
// This way, the above code would render <tag my-attribute="hello world" />.
case HtmlSymbolType.Equals:
case HtmlSymbolType.DoubleQuote:
result.AttributeValueStyle = HtmlAttributeValueStyle.DoubleQuotes;
break;
case HtmlSymbolType.SingleQuote:
result.AttributeValueStyle = HtmlAttributeValueStyle.SingleQuotes;
break;
default:
result.AttributeValueStyle = HtmlAttributeValueStyle.Minimized;
break;
}
}
// Remove first child i.e. foo="
builder.Children.RemoveAt(0);
// Grabbing last child to check if the attribute value is quoted.
var endNode = block.Children.Last();
if (!endNode.IsBlock)
{
var endSpan = (Span)endNode;
// In some malformed cases e.g. <p bar="false', the last Span (false' in the ex.) may contain more
// than a single HTML symbol. Do not ignore those other symbols.
var symbolCount = endSpan.Symbols.Count();
var endSymbol = symbolCount == 1 ? (HtmlSymbol)endSpan.Symbols.First() : null;
// Checking to see if it's a quoted attribute, if so we should remove end quote
if (endSymbol != null && IsQuote(endSymbol))
{
builder.Children.RemoveAt(builder.Children.Count - 1);
}
}
// We need to rebuild the chunk generators of the builder and its children (this is needed to
// ensure we don't do special attribute chunk generation since this is a tag helper).
block = RebuildChunkGenerators(builder.Build(), result.IsBoundAttribute);
// If there's only 1 child at this point its value could be a simple markup span (treated differently than
// block level elements for attributes).
if (block.Children.Count() == 1)
{
var child = block.Children.First() as Span;
if (child != null)
{
// After pulling apart the block we just have a value span.
var spanBuilder = new SpanBuilder(child);
result.AttributeValueNode =
CreateMarkupAttribute(spanBuilder, result.IsBoundNonStringAttribute);
return result;
}
}
var isFirstSpan = true;
result.AttributeValueNode = ConvertToMarkupAttributeBlock(
block,
(parentBlock, span) =>
{
// If the attribute was requested by a tag helper but the corresponding property was not a
// string, then treat its value as code. A non-string value can be any C# value so we need
// to ensure the SyntaxTreeNode reflects that.
if (result.IsBoundNonStringAttribute)
{
// For bound non-string attributes, we'll only allow a transition span to appear at the very
// beginning of the attribute expression. All later transitions would appear as code so that
// they are part of the generated output. E.g.
// key="@value" -> MyTagHelper.key = value
// key=" @value" -> MyTagHelper.key = @value
// key="1 + @case" -> MyTagHelper.key = 1 + @case
// key="@int + @case" -> MyTagHelper.key = int + @case
// key="@(a + b) -> MyTagHelper.key = a + b
// key="4 + @(a + b)" -> MyTagHelper.key = 4 + @(a + b)
if (isFirstSpan && span.Kind == SpanKind.Transition)
{
// do nothing.
}
else
{
var spanBuilder = new SpanBuilder(span);
if (parentBlock.Type == BlockType.Expression &&
(spanBuilder.Kind == SpanKind.Transition ||
spanBuilder.Kind == SpanKind.MetaCode))
{
// Change to a MarkupChunkGenerator so that the '@' \ parenthesis is generated as part of the output.
spanBuilder.ChunkGenerator = new MarkupChunkGenerator();
}
ConfigureNonStringAttribute(spanBuilder);
span = spanBuilder.Build();
}
}
isFirstSpan = false;
return span;
});
return result;
}
private static Block ConvertToMarkupAttributeBlock(
Block block,
Func<Block, Span, Span> createMarkupAttribute)
{
var blockBuilder = new BlockBuilder
{
ChunkGenerator = block.ChunkGenerator,
Type = block.Type
};
foreach (var child in block.Children)
{
SyntaxTreeNode markupAttributeChild;
if (child.IsBlock)
{
markupAttributeChild = ConvertToMarkupAttributeBlock((Block)child, createMarkupAttribute);
}
else
{
markupAttributeChild = createMarkupAttribute(block, (Span)child);
}
blockBuilder.Children.Add(markupAttributeChild);
}
return blockBuilder.Build();
}
private static Block RebuildChunkGenerators(Block block, bool isBound)
{
var builder = new BlockBuilder(block);
// Don't want to rebuild unbound dynamic attributes. They need to run through the conditional attribute
// removal system at runtime. A conditional attribute at the parse tree rewriting level is defined by
// having at least 1 child with a DynamicAttributeBlockChunkGenerator.
if (!isBound &&
block.Children.Any(
child => child.IsBlock &&
((Block)child).ChunkGenerator is DynamicAttributeBlockChunkGenerator))
{
// The parent chunk generator must be removed because it's normally responsible for conditionally
// generating the attribute prefix (class=") and suffix ("). The prefix and suffix concepts aren't
// applicable for the TagHelper use case since the attributes are put into a dictionary like object as
// name value pairs.
builder.ChunkGenerator = ParentChunkGenerator.Null;
return builder.Build();
}
var isDynamic = builder.ChunkGenerator is DynamicAttributeBlockChunkGenerator;
// We don't want any attribute specific logic here, null out the block chunk generator.
if (isDynamic || builder.ChunkGenerator is AttributeBlockChunkGenerator)
{
builder.ChunkGenerator = ParentChunkGenerator.Null;
}
for (var i = 0; i < builder.Children.Count; i++)
{
var child = builder.Children[i];
if (child.IsBlock)
{
// The child is a block, recurse down into the block to rebuild its children
builder.Children[i] = RebuildChunkGenerators((Block)child, isBound);
}
else
{
var childSpan = (Span)child;
ISpanChunkGenerator newChunkGenerator = null;
var literalGenerator = childSpan.ChunkGenerator as LiteralAttributeChunkGenerator;
if (literalGenerator != null)
{
if (literalGenerator.ValueGenerator == null || literalGenerator.ValueGenerator.Value == null)
{
newChunkGenerator = new MarkupChunkGenerator();
}
else
{
newChunkGenerator = literalGenerator.ValueGenerator.Value;
}
}
else if (isDynamic && childSpan.ChunkGenerator == SpanChunkGenerator.Null)
{
// Usually the dynamic chunk generator handles creating the null chunk generators underneath
// it. This doesn't make sense in terms of tag helpers though, we need to change null code
// generators to markup chunk generators.
newChunkGenerator = new MarkupChunkGenerator();
}
// If we have a new chunk generator we'll need to re-build the child
if (newChunkGenerator != null)
{
var childSpanBuilder = new SpanBuilder(childSpan)
{
ChunkGenerator = newChunkGenerator
};
builder.Children[i] = childSpanBuilder.Build();
}
}
}
return builder.Build();
}
private static SourceLocation GetAttributeNameStartLocation(SyntaxTreeNode node)
{
Span span;
var nodeStart = SourceLocation.Undefined;
if (node.IsBlock)
{
span = ((Block)node).FindFirstDescendentSpan();
nodeStart = span.Parent.Start;
}
else
{
span = (Span)node;
nodeStart = span.Start;
}
// Span should never be null here, this should only ever be called if an attribute was successfully parsed.
Debug.Assert(span != null);
// Attributes must have at least one non-whitespace character to represent the tagName (even if its a C#
// expression).
var firstNonWhitespaceSymbol = span
.Symbols
.OfType<HtmlSymbol>()
.First(sym => sym.Type != HtmlSymbolType.WhiteSpace && sym.Type != HtmlSymbolType.NewLine);
return nodeStart + firstNonWhitespaceSymbol.Start;
}
private static Span CreateMarkupAttribute(SpanBuilder builder, bool isBoundNonStringAttribute)
{
Debug.Assert(builder != null);
// If the attribute was requested by a tag helper but the corresponding property was not a string,
// then treat its value as code. A non-string value can be any C# value so we need to ensure the
// SyntaxTreeNode reflects that.
if (isBoundNonStringAttribute)
{
ConfigureNonStringAttribute(builder);
}
return builder.Build();
}
private static bool IsNullOrWhitespaceAttributeValue(SyntaxTreeNode attributeValue)
{
if (attributeValue.IsBlock)
{
foreach (var span in ((Block)attributeValue).Flatten())
{
if (!string.IsNullOrWhiteSpace(span.Content))
{
return false;
}
}
return true;
}
else
{
return string.IsNullOrWhiteSpace(((Span)attributeValue).Content);
}
}
// Determines the full name of the Type of the property corresponding to an attribute with the given name.
private static string GetPropertyType(string name, IEnumerable<TagHelperDescriptor> descriptors)
{
var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors);
return firstBoundAttribute?.TypeName;
}
// Create a TryParseResult for given name, filling in binding details.
private static TryParseResult CreateTryParseResult(string name, IEnumerable<TagHelperDescriptor> descriptors)
{
var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors);
var isBoundAttribute = firstBoundAttribute != null;
var isBoundNonStringAttribute = isBoundAttribute && !firstBoundAttribute.IsStringProperty;
var isMissingDictionaryKey = isBoundAttribute &&
firstBoundAttribute.IsIndexer &&
name.Length == firstBoundAttribute.Name.Length;
return new TryParseResult
{
AttributeName = name,
IsBoundAttribute = isBoundAttribute,
IsBoundNonStringAttribute = isBoundNonStringAttribute,
IsMissingDictionaryKey = isMissingDictionaryKey,
};
}
// Finds first TagHelperAttributeDescriptor matching given name.
private static TagHelperAttributeDescriptor FindFirstBoundAttribute(
string name,
IEnumerable<TagHelperDescriptor> descriptors)
{
// Non-indexers (exact HTML attribute name matches) have higher precedence than indexers (prefix matches).
// Attributes already sorted to ensure this precedence.
var firstBoundAttribute = descriptors
.SelectMany(descriptor => descriptor.Attributes)
.FirstOrDefault(attributeDescriptor => attributeDescriptor.IsNameMatch(name));
return firstBoundAttribute;
}
private static bool IsQuote(HtmlSymbol htmlSymbol)
{
return htmlSymbol.Type == HtmlSymbolType.DoubleQuote ||
htmlSymbol.Type == HtmlSymbolType.SingleQuote;
}
private static void ConfigureNonStringAttribute(SpanBuilder builder)
{
builder.Kind = SpanKind.Code;
builder.EditHandler = new ImplicitExpressionEditHandler(
builder.EditHandler.Tokenizer,
CSharpCodeParser.DefaultKeywords,
acceptTrailingDot: true)
{
AcceptedCharacters = AcceptedCharacters.AnyExceptNewline
};
}
private class TryParseResult
{
public string AttributeName { get; set; }
public SyntaxTreeNode AttributeValueNode { get; set; }
public HtmlAttributeValueStyle AttributeValueStyle { get; set; }
public bool IsBoundAttribute { get; set; }
public bool IsBoundNonStringAttribute { get; set; }
public bool IsMissingDictionaryKey { get; set; }
}
}
}

View File

@ -0,0 +1,89 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal class TagHelperChunkGenerator : ParentChunkGenerator
{
private IEnumerable<TagHelperDescriptor> _tagHelperDescriptors;
/// <summary>
/// Instantiates a new <see cref="TagHelperChunkGenerator"/>.
/// </summary>
/// <param name="tagHelperDescriptors">
/// <see cref="TagHelperDescriptor"/>s associated with the current HTML tag.
/// </param>
public TagHelperChunkGenerator(IEnumerable<TagHelperDescriptor> tagHelperDescriptors)
{
_tagHelperDescriptors = tagHelperDescriptors;
}
public override void GenerateStartParentChunk(Block target, ChunkGeneratorContext context)
{
//var tagHelperBlock = target as TagHelperBlock;
//Debug.Assert(
// tagHelperBlock != null,
// $"A {nameof(TagHelperChunkGenerator)} must only be used with {nameof(TagHelperBlock)}s.");
//var attributes = new List<TagHelperAttributeTracker>();
//// We need to create a chunk generator to create chunks for each of the attributes.
//var chunkGenerator = context.Host.CreateChunkGenerator(
// context.ClassName,
// context.RootNamespace,
// context.SourceFile);
//foreach (var attribute in tagHelperBlock.Attributes)
//{
// ParentChunk attributeChunkValue = null;
// if (attribute.Value != null)
// {
// // Populates the chunk tree with chunks associated with attributes
// attribute.Value.Accept(chunkGenerator);
// var chunks = chunkGenerator.Context.ChunkTreeBuilder.Root.Children;
// var first = chunks.FirstOrDefault();
// attributeChunkValue = new ParentChunk
// {
// Association = first?.Association,
// Children = chunks,
// Start = first == null ? SourceLocation.Zero : first.Start
// };
// }
// var attributeChunk = new TagHelperAttributeTracker(
// attribute.Name,
// attributeChunkValue,
// attribute.ValueStyle);
// attributes.Add(attributeChunk);
// // Reset the chunk tree builder so we can build a new one for the next attribute
// chunkGenerator.Context.ChunkTreeBuilder = new ChunkTreeBuilder();
//}
//var unprefixedTagName = tagHelperBlock.TagName.Substring(_tagHelperDescriptors.First().Prefix.Length);
//context.ChunkTreeBuilder.StartParentChunk(
// new TagHelperChunk(
// unprefixedTagName,
// tagHelperBlock.TagMode,
// attributes,
// _tagHelperDescriptors),
// target,
// topLevel: false);
}
public override void GenerateEndParentChunk(Block target, ChunkGeneratorContext context)
{
//context.ChunkTreeBuilder.EndParentChunk();
}
}
}

View File

@ -0,0 +1,251 @@
// 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;
using System.Linq;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// A metadata class describing a tag helper.
/// </summary>
internal class TagHelperDescriptor
{
private string _prefix = string.Empty;
private string _tagName;
private string _typeName;
private string _assemblyName;
private IDictionary<string, string> _propertyBag;
private IEnumerable<TagHelperAttributeDescriptor> _attributes =
Enumerable.Empty<TagHelperAttributeDescriptor>();
private IEnumerable<TagHelperRequiredAttributeDescriptor> _requiredAttributes =
Enumerable.Empty<TagHelperRequiredAttributeDescriptor>();
/// <summary>
/// Creates a new <see cref="TagHelperDescriptor"/>.
/// </summary>
public TagHelperDescriptor()
{
}
/// <summary>
/// Creates a shallow copy of the given <see cref="TagHelperDescriptor"/>.
/// </summary>
/// <param name="descriptor">The <see cref="TagHelperDescriptor"/> to copy.</param>
public TagHelperDescriptor(TagHelperDescriptor descriptor)
{
Prefix = descriptor.Prefix;
TagName = descriptor.TagName;
TypeName = descriptor.TypeName;
AssemblyName = descriptor.AssemblyName;
Attributes = descriptor.Attributes;
RequiredAttributes = descriptor.RequiredAttributes;
AllowedChildren = descriptor.AllowedChildren;
RequiredParent = descriptor.RequiredParent;
TagStructure = descriptor.TagStructure;
DesignTimeDescriptor = descriptor.DesignTimeDescriptor;
foreach (var property in descriptor.PropertyBag)
{
PropertyBag.Add(property.Key, property.Value);
}
}
/// <summary>
/// Text used as a required prefix when matching HTML start and end tags in the Razor source to available
/// tag helpers.
/// </summary>
public string Prefix
{
get
{
return _prefix;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_prefix = value;
}
}
/// <summary>
/// The tag name that the tag helper should target.
/// </summary>
public string TagName
{
get
{
return _tagName;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_tagName = value;
}
}
/// <summary>
/// The full tag name that is required for the tag helper to target an HTML element.
/// </summary>
/// <remarks>This is equivalent to <see cref="Prefix"/> and <see cref="TagName"/> concatenated.</remarks>
public string FullTagName
{
get
{
return Prefix + TagName;
}
}
/// <summary>
/// The full name of the tag helper class.
/// </summary>
public string TypeName
{
get
{
return _typeName;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_typeName = value;
}
}
/// <summary>
/// The name of the assembly containing the tag helper class.
/// </summary>
public string AssemblyName
{
get
{
return _assemblyName;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_assemblyName = value;
}
}
/// <summary>
/// The list of attributes the tag helper expects.
/// </summary>
public IEnumerable<TagHelperAttributeDescriptor> Attributes
{
get
{
return _attributes;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_attributes = value;
}
}
/// <summary>
/// The list of required attribute names the tag helper expects to target an element.
/// </summary>
/// <remarks>
/// <c>*</c> at the end of an attribute name acts as a prefix match.
/// </remarks>
public IEnumerable<TagHelperRequiredAttributeDescriptor> RequiredAttributes
{
get
{
return _requiredAttributes;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_requiredAttributes = value;
}
}
/// <summary>
/// Get the names of elements allowed as children.
/// </summary>
/// <remarks><c>null</c> indicates all children are allowed.</remarks>
public IEnumerable<string> AllowedChildren { get; set; }
/// <summary>
/// Get the name of the HTML element required as the immediate parent.
/// </summary>
/// <remarks><c>null</c> indicates no restriction on parent tag.</remarks>
public string RequiredParent { get; set; }
/// <summary>
/// The expected tag structure.
/// </summary>
/// <remarks>
/// If <see cref="TagStructure.Unspecified"/> and no other tag helpers applying to the same element specify
/// their <see cref="TagStructure"/> the <see cref="TagStructure.NormalOrSelfClosing"/> behavior is used:
/// <para>
/// <code>
/// &lt;my-tag-helper&gt;&lt;/my-tag-helper&gt;
/// &lt;!-- OR --&gt;
/// &lt;my-tag-helper /&gt;
/// </code>
/// Otherwise, if another tag helper applying to the same element does specify their behavior, that behavior
/// is used.
/// </para>
/// <para>
/// If <see cref="TagStructure.WithoutEndTag"/> HTML elements can be written in the following formats:
/// <code>
/// &lt;my-tag-helper&gt;
/// &lt;!-- OR --&gt;
/// &lt;my-tag-helper /&gt;
/// </code>
/// </para>
/// </remarks>
public TagStructure TagStructure { get; set; }
/// <summary>
/// The <see cref="TagHelperDesignTimeDescriptor"/> that contains design time information about this
/// tag helper.
/// </summary>
public TagHelperDesignTimeDescriptor DesignTimeDescriptor { get; set; }
/// <summary>
/// A dictionary containing additional information about the <see cref="TagHelperDescriptor"/>.
/// </summary>
public IDictionary<string, string> PropertyBag
{
get
{
if (_propertyBag == null)
{
_propertyBag = new Dictionary<string, string>();
}
return _propertyBag;
}
}
}
}

View File

@ -0,0 +1,105 @@
// 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;
using System.Linq;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// An <see cref="IEqualityComparer{TagHelperDescriptor}"/> used to check equality between
/// two <see cref="TagHelperDescriptor"/>s.
/// </summary>
internal class TagHelperDescriptorComparer : IEqualityComparer<TagHelperDescriptor>
{
/// <summary>
/// A default instance of the <see cref="TagHelperDescriptorComparer"/>.
/// </summary>
public static readonly TagHelperDescriptorComparer Default = new TagHelperDescriptorComparer();
/// <summary>
/// Initializes a new <see cref="TagHelperDescriptorComparer"/> instance.
/// </summary>
protected TagHelperDescriptorComparer()
{
}
/// <inheritdoc />
/// <remarks>
/// Determines equality based on <see cref="TagHelperDescriptor.TypeName"/>,
/// <see cref="TagHelperDescriptor.AssemblyName"/>, <see cref="TagHelperDescriptor.TagName"/>,
/// <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>
public virtual bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}
return descriptorX != null &&
string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal) &&
string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal) &&
string.Equals(
descriptorX.RequiredParent,
descriptorY.RequiredParent,
StringComparison.OrdinalIgnoreCase) &&
Enumerable.SequenceEqual(
descriptorX.RequiredAttributes.OrderBy(attribute => attribute.Name, StringComparer.OrdinalIgnoreCase),
descriptorY.RequiredAttributes.OrderBy(attribute => attribute.Name, StringComparer.OrdinalIgnoreCase),
TagHelperRequiredAttributeDescriptorComparer.Default) &&
(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 &&
Enumerable.SequenceEqual(
descriptorX.PropertyBag.OrderBy(propertyX => propertyX.Key, StringComparer.Ordinal),
descriptorY.PropertyBag.OrderBy(propertyY => propertyY.Key, StringComparer.Ordinal));
}
/// <inheritdoc />
public virtual int GetHashCode(TagHelperDescriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(descriptor.TypeName, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.TagName, StringComparer.OrdinalIgnoreCase);
hashCodeCombiner.Add(descriptor.AssemblyName, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.RequiredParent, StringComparer.OrdinalIgnoreCase);
hashCodeCombiner.Add(descriptor.TagStructure);
var attributes = descriptor.RequiredAttributes.OrderBy(
attribute => attribute.Name,
StringComparer.OrdinalIgnoreCase);
foreach (var attribute in attributes)
{
hashCodeCombiner.Add(TagHelperRequiredAttributeDescriptorComparer.Default.GetHashCode(attribute));
}
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

@ -0,0 +1,133 @@
// 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;
using System.Linq;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// Enables retrieval of <see cref="TagHelperDescriptor"/>'s.
/// </summary>
internal class TagHelperDescriptorProvider
{
public const string ElementCatchAllTarget = "*";
private IDictionary<string, HashSet<TagHelperDescriptor>> _registrations;
private string _tagHelperPrefix;
/// <summary>
/// Instantiates a new instance of the <see cref="TagHelperDescriptorProvider"/>.
/// </summary>
/// <param name="descriptors">The descriptors that the <see cref="TagHelperDescriptorProvider"/> will pull from.</param>
public TagHelperDescriptorProvider(IEnumerable<TagHelperDescriptor> descriptors)
{
_registrations = new Dictionary<string, HashSet<TagHelperDescriptor>>(StringComparer.OrdinalIgnoreCase);
// Populate our registrations
foreach (var descriptor in descriptors)
{
Register(descriptor);
}
}
/// <summary>
/// Gets all tag helpers that match the given <paramref name="tagName"/>.
/// </summary>
/// <param name="tagName">The name of the HTML tag to match. Providing a '*' tag name
/// retrieves catch-all <see cref="TagHelperDescriptor"/>s (descriptors that target every tag).</param>
/// <param name="attributes">Attributes the HTML element must contain to match.</param>
/// <param name="parentTagName">The parent tag name of the given <paramref name="tagName"/> tag.</param>
/// <returns><see cref="TagHelperDescriptor"/>s that apply to the given <paramref name="tagName"/>.
/// Will return an empty <see cref="Enumerable" /> if no <see cref="TagHelperDescriptor"/>s are
/// found.</returns>
public IEnumerable<TagHelperDescriptor> GetDescriptors(
string tagName,
IEnumerable<KeyValuePair<string, string>> attributes,
string parentTagName)
{
if (!string.IsNullOrEmpty(_tagHelperPrefix) &&
(tagName.Length <= _tagHelperPrefix.Length ||
!tagName.StartsWith(_tagHelperPrefix, StringComparison.OrdinalIgnoreCase)))
{
// The tagName doesn't have the tag helper prefix, we can short circuit.
return Enumerable.Empty<TagHelperDescriptor>();
}
HashSet<TagHelperDescriptor> catchAllDescriptors;
IEnumerable<TagHelperDescriptor> descriptors;
// Ensure there's a HashSet to use.
if (!_registrations.TryGetValue(ElementCatchAllTarget, out catchAllDescriptors))
{
descriptors = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
}
else
{
descriptors = catchAllDescriptors;
}
// If we have a tag name associated with the requested name, we need to combine matchingDescriptors
// with all the catch-all descriptors.
HashSet<TagHelperDescriptor> matchingDescriptors;
if (_registrations.TryGetValue(tagName, out matchingDescriptors))
{
descriptors = matchingDescriptors.Concat(descriptors);
}
var applicableDescriptors = new List<TagHelperDescriptor>();
foreach (var descriptor in descriptors)
{
if (HasRequiredAttributes(descriptor, attributes) &&
HasRequiredParentTag(descriptor, parentTagName))
{
applicableDescriptors.Add(descriptor);
}
}
return applicableDescriptors;
}
private bool HasRequiredParentTag(
TagHelperDescriptor descriptor,
string parentTagName)
{
return descriptor.RequiredParent == null ||
string.Equals(parentTagName, descriptor.RequiredParent, StringComparison.OrdinalIgnoreCase);
}
private bool HasRequiredAttributes(
TagHelperDescriptor descriptor,
IEnumerable<KeyValuePair<string, string>> attributes)
{
return descriptor.RequiredAttributes.All(
requiredAttribute => attributes.Any(
attribute => requiredAttribute.IsMatch(attribute.Key, attribute.Value)));
}
private void Register(TagHelperDescriptor descriptor)
{
HashSet<TagHelperDescriptor> descriptorSet;
if (_tagHelperPrefix == null)
{
_tagHelperPrefix = descriptor.Prefix;
}
var registrationKey =
string.Equals(descriptor.TagName, ElementCatchAllTarget, StringComparison.Ordinal) ?
ElementCatchAllTarget :
descriptor.FullTagName;
// Ensure there's a HashSet to add the descriptor to.
if (!_registrations.TryGetValue(registrationKey, out descriptorSet))
{
descriptorSet = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
_registrations[registrationKey] = descriptorSet;
}
descriptorSet.Add(descriptor);
}
}
}

View File

@ -0,0 +1,54 @@
// 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.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// Contains information needed to resolve <see cref="TagHelperDescriptor"/>s.
/// </summary>
internal class TagHelperDescriptorResolutionContext
{
// Internal for testing purposes
internal TagHelperDescriptorResolutionContext(IEnumerable<TagHelperDirectiveDescriptor> directiveDescriptors)
: this(directiveDescriptors, new ErrorSink())
{
}
/// <summary>
/// Instantiates a new instance of <see cref="TagHelperDescriptorResolutionContext"/>.
/// </summary>
/// <param name="directiveDescriptors"><see cref="TagHelperDirectiveDescriptor"/>s used to resolve
/// <see cref="TagHelperDescriptor"/>s.</param>
/// <param name="errorSink">Used to aggregate <see cref="RazorError"/>s.</param>
public TagHelperDescriptorResolutionContext(
IEnumerable<TagHelperDirectiveDescriptor> directiveDescriptors,
ErrorSink errorSink)
{
if (directiveDescriptors == null)
{
throw new ArgumentNullException(nameof(directiveDescriptors));
}
if (errorSink == null)
{
throw new ArgumentNullException(nameof(errorSink));
}
DirectiveDescriptors = new List<TagHelperDirectiveDescriptor>(directiveDescriptors);
ErrorSink = errorSink;
}
/// <summary>
/// <see cref="TagHelperDirectiveDescriptor"/>s used to resolve <see cref="TagHelperDescriptor"/>s.
/// </summary>
public IList<TagHelperDirectiveDescriptor> DirectiveDescriptors { get; private set; }
/// <summary>
/// Used to aggregate <see cref="RazorError"/>s.
/// </summary>
public ErrorSink ErrorSink { get; private set; }
}
}

View File

@ -0,0 +1,29 @@
// 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.
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// A metadata class containing design time information about a tag helper.
/// </summary>
internal class TagHelperDesignTimeDescriptor
{
/// <summary>
/// A summary of how to use a tag helper.
/// </summary>
public string Summary { get; set; }
/// <summary>
/// Remarks about how to use a tag helper.
/// </summary>
public string Remarks { get; set; }
/// <summary>
/// The HTML element a tag helper may output.
/// </summary>
/// <remarks>
/// In IDEs supporting IntelliSense, may override the HTML information provided at design time.
/// </remarks>
public string OutputElementHint { get; set; }
}
}

View File

@ -0,0 +1,45 @@
// 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;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// Contains information needed to resolve <see cref="TagHelperDescriptor"/>s.
/// </summary>
internal class TagHelperDirectiveDescriptor
{
private string _directiveText;
/// <summary>
/// A <see cref="string"/> used to find tag helper <see cref="System.Type"/>s.
/// </summary>
public string DirectiveText
{
get
{
return _directiveText;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_directiveText = value;
}
}
/// <summary>
/// The <see cref="SourceLocation"/> of the directive.
/// </summary>
public SourceLocation Location { get; set; } = SourceLocation.Zero;
/// <summary>
/// The <see cref="TagHelperDirectiveType"/> of this directive.
/// </summary>
public TagHelperDirectiveType DirectiveType { get; set; }
}
}

View File

@ -0,0 +1,151 @@
// 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.AspNetCore.Razor.Evolution.Legacy
{
internal class TagHelperDirectiveSpanVisitor
{
private readonly ITagHelperDescriptorResolver _descriptorResolver;
private readonly ErrorSink _errorSink;
private List<TagHelperDirectiveDescriptor> _directiveDescriptors;
public int Order { get; }
public RazorEngine Engine { get; set; }
// Internal for testing use
internal TagHelperDirectiveSpanVisitor(ITagHelperDescriptorResolver descriptorResolver)
: this(descriptorResolver, new ErrorSink())
{
}
public TagHelperDirectiveSpanVisitor(
ITagHelperDescriptorResolver descriptorResolver,
ErrorSink errorSink)
{
if (descriptorResolver == null)
{
throw new ArgumentNullException(nameof(descriptorResolver));
}
if (errorSink == null)
{
throw new ArgumentNullException(nameof(errorSink));
}
_descriptorResolver = descriptorResolver;
_errorSink = errorSink;
}
public IEnumerable<TagHelperDescriptor> GetDescriptors(Block root)
{
if (root == null)
{
throw new ArgumentNullException(nameof(root));
}
_directiveDescriptors = new List<TagHelperDirectiveDescriptor>();
// This will recurse through the syntax tree.
VisitBlock(root);
var resolutionContext = GetTagHelperDescriptorResolutionContext(_directiveDescriptors, _errorSink);
var descriptors = _descriptorResolver.Resolve(resolutionContext);
return descriptors;
}
// Allows MVC a chance to override the TagHelperDescriptorResolutionContext
protected virtual TagHelperDescriptorResolutionContext GetTagHelperDescriptorResolutionContext(
IEnumerable<TagHelperDirectiveDescriptor> descriptors,
ErrorSink errorSink)
{
if (descriptors == null)
{
throw new ArgumentNullException(nameof(descriptors));
}
if (errorSink == null)
{
throw new ArgumentNullException(nameof(errorSink));
}
return new TagHelperDescriptorResolutionContext(descriptors, errorSink);
}
public void VisitBlock(Block block)
{
for (var i = 0; i < block.Children.Count; i++)
{
var child = block.Children[i];
if (child.IsBlock)
{
VisitBlock((Block)child);
}
else
{
VisitSpan((Span)child);
}
}
}
public void VisitSpan(Span span)
{
if (span == null)
{
throw new ArgumentNullException(nameof(span));
}
string directiveText;
TagHelperDirectiveType directiveType;
var addTagHelperChunkGenerator = span.ChunkGenerator as AddTagHelperChunkGenerator;
var removeTagHelperChunkGenerator = span.ChunkGenerator as RemoveTagHelperChunkGenerator;
var tagHelperPrefixChunkGenerator = span.ChunkGenerator as TagHelperPrefixDirectiveChunkGenerator;
if (addTagHelperChunkGenerator != null)
{
directiveType = TagHelperDirectiveType.AddTagHelper;
directiveText = addTagHelperChunkGenerator.LookupText;
}
else if (removeTagHelperChunkGenerator != null)
{
directiveType = TagHelperDirectiveType.RemoveTagHelper;
directiveText = removeTagHelperChunkGenerator.LookupText;
}
else if (tagHelperPrefixChunkGenerator != null)
{
directiveType = TagHelperDirectiveType.TagHelperPrefix;
directiveText = tagHelperPrefixChunkGenerator.Prefix;
}
else
{
// Not a chunk generator that we're interested in.
return;
}
directiveText = directiveText.Trim();
var startOffset = span.Content.IndexOf(directiveText, StringComparison.Ordinal);
var offsetContent = span.Content.Substring(0, startOffset);
var offsetTextLocation = SourceLocation.Advance(span.Start, offsetContent);
var directiveDescriptor = new TagHelperDirectiveDescriptor
{
DirectiveText = directiveText,
Location = offsetTextLocation,
DirectiveType = directiveType
};
_directiveDescriptors.Add(directiveDescriptor);
}
public RazorSyntaxTree Execute(RazorCodeDocument codeDocument, RazorSyntaxTree syntaxTree)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,26 @@
// 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.
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// The type of tag helper directive.
/// </summary>
internal enum TagHelperDirectiveType
{
/// <summary>
/// An <c>@addTagHelper</c> directive.
/// </summary>
AddTagHelper,
/// <summary>
/// A <c>@removeTagHelper</c> directive.
/// </summary>
RemoveTagHelper,
/// <summary>
/// A <c>@tagHelperPrefix</c> directive.
/// </summary>
TagHelperPrefix
}
}

View File

@ -0,0 +1,872 @@
// 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;
using System.Diagnostics;
using System.Linq;
using System.Text;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal class TagHelperParseTreeRewriter
{
// Internal for testing.
// Null characters are invalid markup for HTML attribute values.
internal static readonly string InvalidAttributeValueMarker = "\0";
// From http://dev.w3.org/html5/spec/Overview.html#elements-0
private static readonly HashSet<string> VoidElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"area",
"base",
"br",
"col",
"command",
"embed",
"hr",
"img",
"input",
"keygen",
"link",
"meta",
"param",
"source",
"track",
"wbr"
};
private readonly List<KeyValuePair<string, string>> _htmlAttributeTracker;
private readonly StringBuilder _attributeValueBuilder;
private readonly TagHelperDescriptorProvider _provider;
private readonly Stack<TagBlockTracker> _trackerStack;
private readonly Stack<BlockBuilder> _blockStack;
private TagHelperBlockTracker _currentTagHelperTracker;
private BlockBuilder _currentBlock;
private string _currentParentTagName;
public TagHelperParseTreeRewriter(TagHelperDescriptorProvider provider)
{
_provider = provider;
_trackerStack = new Stack<TagBlockTracker>();
_blockStack = new Stack<BlockBuilder>();
_attributeValueBuilder = new StringBuilder();
_htmlAttributeTracker = new List<KeyValuePair<string, string>>();
}
public Block Rewrite(Block syntaxTree, ErrorSink errorSink)
{
RewriteTags(syntaxTree, errorSink, depth: 0);
var rewritten = _currentBlock.Build();
return rewritten;
}
private void RewriteTags(Block input, ErrorSink errorSink, int depth)
{
// We want to start a new block without the children from existing (we rebuild them).
TrackBlock(new BlockBuilder
{
Type = input.Type,
ChunkGenerator = input.ChunkGenerator
});
var activeTrackers = _trackerStack.Count;
foreach (var child in input.Children)
{
if (child.IsBlock)
{
var childBlock = (Block)child;
if (childBlock.Type == BlockType.Tag)
{
if (TryRewriteTagHelper(childBlock, errorSink))
{
continue;
}
else
{
// Non-TagHelper tag.
ValidateParentAllowsPlainTag(childBlock, errorSink);
TrackTagBlock(childBlock, depth);
}
// 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.
}
else
{
// We're not an Html tag so iterate through children recursively.
RewriteTags(childBlock, errorSink, depth + 1);
continue;
}
}
else
{
ValidateParentAllowsContent((Span)child, errorSink);
}
// At this point the child is a Span or Block with Type BlockType.Tag that doesn't happen to be a
// tag helper.
// Add the child to current block.
_currentBlock.Children.Add(child);
}
// We captured the number of active tag helpers at the start of our logic, it should be the same. If not
// it means that there are malformed tag helpers at the top of our stack.
if (activeTrackers != _trackerStack.Count)
{
// Malformed tag helpers built here will be tag helpers that do not have end tags in the current block
// scope. Block scopes are special cases in Razor such as @<p> would cause an error because there's no
// matching end </p> tag in the template block scope and therefore doesn't make sense as a tag helper.
BuildMalformedTagHelpers(_trackerStack.Count - activeTrackers, errorSink);
Debug.Assert(activeTrackers == _trackerStack.Count);
}
BuildCurrentlyTrackedBlock();
}
private void TrackTagBlock(Block childBlock, int depth)
{
var tagName = GetTagName(childBlock);
// Don't want to track incomplete tags that have no tag name.
if (string.IsNullOrWhiteSpace(tagName))
{
return;
}
if (IsEndTag(childBlock))
{
var parentTracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null;
if (parentTracker != null &&
!parentTracker.IsTagHelper &&
depth == parentTracker.Depth &&
string.Equals(parentTracker.TagName, tagName, StringComparison.OrdinalIgnoreCase))
{
PopTrackerStack();
}
}
else if (!VoidElements.Contains(tagName) && !IsSelfClosing(childBlock))
{
// If it's not a void element and it's not self-closing then we need to create a tag
// tracker for it.
var tracker = new TagBlockTracker(tagName, isTagHelper: false, depth: depth);
PushTrackerStack(tracker);
}
}
private bool TryRewriteTagHelper(Block tagBlock, ErrorSink errorSink)
{
// Get tag name of the current block (doesn't matter if it's an end or start tag)
var tagName = GetTagName(tagBlock);
// Could not determine tag name, it can't be a TagHelper, continue on and track the element.
if (tagName == null)
{
return false;
}
var descriptors = Enumerable.Empty<TagHelperDescriptor>();
if (!IsPotentialTagHelper(tagName, tagBlock))
{
return false;
}
var tracker = _currentTagHelperTracker;
var tagNameScope = tracker?.TagName ?? string.Empty;
if (!IsEndTag(tagBlock))
{
// We're now in a start tag block, we first need to see if the tag block is a tag helper.
var providedAttributes = GetAttributeNameValuePairs(tagBlock);
descriptors = _provider.GetDescriptors(tagName, providedAttributes, _currentParentTagName);
// If there aren't any TagHelperDescriptors registered then we aren't a TagHelper
if (!descriptors.Any())
{
// If the current tag matches the current TagHelper scope it means the parent TagHelper matched
// all the required attributes but the current one did not; therefore, we need to increment the
// OpenMatchingTags counter for current the TagHelperBlock so we don't end it too early.
// ex: <myth req="..."><myth></myth></myth> We don't want the first myth to close on the inside
// tag.
if (string.Equals(tagNameScope, tagName, StringComparison.OrdinalIgnoreCase))
{
tracker.OpenMatchingTags++;
}
return false;
}
ValidateParentAllowsTagHelper(tagName, tagBlock, errorSink);
ValidateDescriptors(descriptors, tagName, tagBlock, errorSink);
// We're in a start TagHelper block.
var validTagStructure = ValidateTagSyntax(tagName, tagBlock, errorSink);
var builder = TagHelperBlockRewriter.Rewrite(
tagName,
validTagStructure,
tagBlock,
descriptors,
errorSink);
// Track the original start tag so the editor knows where each piece of the TagHelperBlock lies
// for formatting.
builder.SourceStartTag = tagBlock;
// Found a new tag helper block
TrackTagHelperBlock(builder);
// If it's a non-content expecting block then we don't have to worry about nested children within the
// tag. Complete it.
if (builder.TagMode == TagMode.SelfClosing || builder.TagMode == TagMode.StartTagOnly)
{
BuildCurrentlyTrackedTagHelperBlock(endTag: null);
}
}
else
{
// Validate that our end tag matches the currently scoped tag, if not we may need to error.
if (tagNameScope.Equals(tagName, StringComparison.OrdinalIgnoreCase))
{
// If there are additional end tags required before we can build our block it means we're in a
// situation like this: <myth req="..."><myth></myth></myth> where we're at the inside </myth>.
if (tracker.OpenMatchingTags > 0)
{
tracker.OpenMatchingTags--;
return false;
}
ValidateTagSyntax(tagName, tagBlock, errorSink);
BuildCurrentlyTrackedTagHelperBlock(tagBlock);
}
else
{
descriptors = _provider.GetDescriptors(
tagName,
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: _currentParentTagName);
// If there are not TagHelperDescriptors associated with the end tag block that also have no
// required attributes then it means we can't be a TagHelper, bail out.
if (!descriptors.Any())
{
return false;
}
var invalidDescriptor = descriptors.FirstOrDefault(
descriptor => descriptor.TagStructure == TagStructure.WithoutEndTag);
if (invalidDescriptor != null)
{
// End tag TagHelper that states it shouldn't have an end tag.
errorSink.OnError(
SourceLocation.Advance(tagBlock.Start, "</"),
LegacyResources.FormatTagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag(
tagName,
invalidDescriptor.TypeName,
invalidDescriptor.TagStructure),
tagName.Length);
return false;
}
// Current tag helper scope does not match the end tag. Attempt to recover the tag
// helper by looking up the previous tag helper scopes for a matching tag. If we
// can't recover it means there was no corresponding tag helper start tag.
if (TryRecoverTagHelper(tagName, tagBlock, errorSink))
{
ValidateParentAllowsTagHelper(tagName, tagBlock, errorSink);
ValidateTagSyntax(tagName, tagBlock, errorSink);
// Successfully recovered, move onto the next element.
}
else
{
// Could not recover, the end tag helper has no corresponding start tag, create
// an error based on the current childBlock.
errorSink.OnError(
SourceLocation.Advance(tagBlock.Start, "</"),
LegacyResources.FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper(tagName),
tagName.Length);
return false;
}
}
}
return true;
}
// Internal for testing
internal IEnumerable<KeyValuePair<string, string>> GetAttributeNameValuePairs(Block tagBlock)
{
// Need to calculate how many children we should take that represent the attributes.
var childrenOffset = IsPartialTag(tagBlock) ? 0 : 1;
var childCount = tagBlock.Children.Count - childrenOffset;
if (childCount <= 1)
{
return Enumerable.Empty<KeyValuePair<string, string>>();
}
_htmlAttributeTracker.Clear();
var attributes = _htmlAttributeTracker;
for (var i = 1; i < childCount; i++)
{
var child = tagBlock.Children[i];
Span childSpan;
if (child.IsBlock)
{
var childBlock = (Block)child;
if (childBlock.Type != BlockType.Markup)
{
// Anything other than markup blocks in the attribute area of tags mangles following attributes.
// It's also not supported by TagHelpers, bail early to avoid creating bad attribute value pairs.
break;
}
childSpan = childBlock.FindFirstDescendentSpan();
if (childSpan == null)
{
_attributeValueBuilder.Append(InvalidAttributeValueMarker);
continue;
}
// We can assume the first span will always contain attributename=" and the last span will always
// contain the final quote. Therefore, if the values not quoted there's no ending quote to skip.
var childOffset = 0;
if (childSpan.Symbols.Count > 0)
{
var potentialQuote = childSpan.Symbols[childSpan.Symbols.Count - 1] as HtmlSymbol;
if (potentialQuote != null &&
(potentialQuote.Type == HtmlSymbolType.DoubleQuote ||
potentialQuote.Type == HtmlSymbolType.SingleQuote))
{
childOffset = 1;
}
}
for (var j = 1; j < childBlock.Children.Count - childOffset; j++)
{
var valueChild = childBlock.Children[j];
if (valueChild.IsBlock)
{
_attributeValueBuilder.Append(InvalidAttributeValueMarker);
}
else
{
var valueChildSpan = (Span)valueChild;
for (var k = 0; k < valueChildSpan.Symbols.Count; k++)
{
_attributeValueBuilder.Append(valueChildSpan.Symbols[k].Content);
}
}
}
}
else
{
childSpan = (Span)child;
var afterEquals = false;
var atValue = false;
var endValueMarker = childSpan.Symbols.Count;
// Entire attribute is a string
for (var j = 0; j < endValueMarker; j++)
{
var htmlSymbol = (HtmlSymbol)childSpan.Symbols[j];
if (!afterEquals)
{
afterEquals = htmlSymbol.Type == HtmlSymbolType.Equals;
continue;
}
if (!atValue)
{
atValue = htmlSymbol.Type != HtmlSymbolType.WhiteSpace &&
htmlSymbol.Type != HtmlSymbolType.NewLine;
if (atValue)
{
if (htmlSymbol.Type == HtmlSymbolType.DoubleQuote ||
htmlSymbol.Type == HtmlSymbolType.SingleQuote)
{
endValueMarker--;
}
else
{
// Current symbol is considered the value (unquoted). Add its content to the
// attribute value builder before we move past it.
_attributeValueBuilder.Append(htmlSymbol.Content);
}
}
continue;
}
_attributeValueBuilder.Append(htmlSymbol.Content);
}
}
var start = 0;
for (; start < childSpan.Content.Length; start++)
{
if (!char.IsWhiteSpace(childSpan.Content[start]))
{
break;
}
}
var end = start;
for (; end < childSpan.Content.Length; end++)
{
if (childSpan.Content[end] == '=')
{
break;
}
}
var attributeName = childSpan.Content.Substring(start, end - start);
var attributeValue = _attributeValueBuilder.ToString();
var attribute = new KeyValuePair<string, string>(attributeName, attributeValue);
attributes.Add(attribute);
_attributeValueBuilder.Clear();
}
return attributes;
}
private bool HasAllowedChildren()
{
var currentTracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null;
// If the current tracker is not a TagHelper then there's no AllowedChildren to enforce.
if (currentTracker == null || !currentTracker.IsTagHelper)
{
return false;
}
return _currentTagHelperTracker.AllowedChildren != null;
}
private void ValidateParentAllowsContent(Span child, ErrorSink errorSink)
{
if (HasAllowedChildren())
{
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 allowedChildren = _currentTagHelperTracker.AllowedChildren;
var allowedChildrenString = string.Join(", ", allowedChildren);
errorSink.OnError(
errorStart,
LegacyResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent(
_currentTagHelperTracker.TagName,
allowedChildrenString),
length);
}
}
}
private void ValidateParentAllowsPlainTag(Block tagBlock, ErrorSink errorSink)
{
var tagName = GetTagName(tagBlock);
// Treat partial tags such as '</' which have no tag names as content.
if (string.IsNullOrEmpty(tagName))
{
Debug.Assert(tagBlock.Children.First() is Span);
ValidateParentAllowsContent((Span)tagBlock.Children.First(), errorSink);
return;
}
var currentTracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null;
if (HasAllowedChildren() &&
!_currentTagHelperTracker.AllowedChildren.Contains(tagName, StringComparer.OrdinalIgnoreCase))
{
OnAllowedChildrenTagError(_currentTagHelperTracker, tagName, tagBlock, errorSink);
}
}
private void ValidateParentAllowsTagHelper(string tagName, Block tagBlock, ErrorSink errorSink)
{
if (HasAllowedChildren() &&
!_currentTagHelperTracker.PrefixedAllowedChildren.Contains(tagName, StringComparer.OrdinalIgnoreCase))
{
OnAllowedChildrenTagError(_currentTagHelperTracker, tagName, tagBlock, errorSink);
}
}
private static void OnAllowedChildrenTagError(
TagHelperBlockTracker tracker,
string tagName,
Block tagBlock,
ErrorSink errorSink)
{
var allowedChildrenString = string.Join(", ", tracker.AllowedChildren);
var errorMessage = LegacyResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag(
tagName,
tracker.TagName,
allowedChildrenString);
var errorStart = GetTagDeclarationErrorStart(tagBlock);
errorSink.OnError(errorStart, errorMessage, tagName.Length);
}
private static void ValidateDescriptors(
IEnumerable<TagHelperDescriptor> descriptors,
string tagName,
Block tagBlock,
ErrorSink errorSink)
{
// Ensure that all descriptors associated with this tag have appropriate TagStructures. Cannot have
// multiple descriptors that expect different TagStructures (other than TagStructure.Unspecified).
TagHelperDescriptor baseDescriptor = null;
foreach (var descriptor in descriptors)
{
if (descriptor.TagStructure != TagStructure.Unspecified)
{
// Can't have a set of TagHelpers that expect different structures.
if (baseDescriptor != null && baseDescriptor.TagStructure != descriptor.TagStructure)
{
errorSink.OnError(
tagBlock.Start,
LegacyResources.FormatTagHelperParseTreeRewriter_InconsistentTagStructure(
baseDescriptor.TypeName,
descriptor.TypeName,
tagName,
nameof(TagHelperDescriptor.TagStructure)),
tagBlock.Length);
}
baseDescriptor = descriptor;
}
}
}
private static bool ValidateTagSyntax(string tagName, Block tag, ErrorSink errorSink)
{
// We assume an invalid syntax until we verify that the tag meets all of our "valid syntax" criteria.
if (IsPartialTag(tag))
{
var errorStart = GetTagDeclarationErrorStart(tag);
errorSink.OnError(
errorStart,
LegacyResources.FormatTagHelpersParseTreeRewriter_MissingCloseAngle(tagName),
tagName.Length);
return false;
}
return true;
}
private static SourceLocation GetTagDeclarationErrorStart(Block tagBlock)
{
var advanceBy = IsEndTag(tagBlock) ? "</" : "<";
return SourceLocation.Advance(tagBlock.Start, advanceBy);
}
private static bool IsPartialTag(Block tagBlock)
{
// No need to validate the tag end because in order to be a tag block it must start with '<'.
var tagEnd = tagBlock.Children[tagBlock.Children.Count - 1] as Span;
// If our tag end is not a markup span it means it's some sort of code SyntaxTreeNode (not a valid format)
if (tagEnd != null && tagEnd.Kind == SpanKind.Markup)
{
var endSymbol = tagEnd.Symbols.Count > 0 ?
tagEnd.Symbols[tagEnd.Symbols.Count - 1] as HtmlSymbol :
null;
if (endSymbol != null && endSymbol.Type == HtmlSymbolType.CloseAngle)
{
return false;
}
}
return true;
}
private void BuildCurrentlyTrackedBlock()
{
// Going to remove the current BlockBuilder from the stack because it's complete.
var currentBlock = _blockStack.Pop();
// If there are block stacks left it means we're not at the root.
if (_blockStack.Count > 0)
{
// Grab the next block in line so we can continue managing its children (it's not done).
var previousBlock = _blockStack.Peek();
// We've finished the currentBlock so build it and add it to its parent.
previousBlock.Children.Add(currentBlock.Build());
// Update the _currentBlock to point at the last tracked block because it's not complete.
_currentBlock = previousBlock;
}
else
{
_currentBlock = currentBlock;
}
}
private void BuildCurrentlyTrackedTagHelperBlock(Block endTag)
{
Debug.Assert(_trackerStack.Any(tracker => tracker.IsTagHelper));
// We need to pop all trackers until we reach our TagHelperBlock. We can throw away any non-TagHelper
// trackers because they don't need to be well-formed.
TagHelperBlockTracker tagHelperTracker;
do
{
tagHelperTracker = PopTrackerStack() as TagHelperBlockTracker;
}
while (tagHelperTracker == null);
// Track the original end tag so the editor knows where each piece of the TagHelperBlock lies
// for formatting.
tagHelperTracker.Builder.SourceEndTag = endTag;
_currentTagHelperTracker =
(TagHelperBlockTracker)_trackerStack.FirstOrDefault(tagBlockTracker => tagBlockTracker.IsTagHelper);
BuildCurrentlyTrackedBlock();
}
private bool IsPotentialTagHelper(string tagName, Block childBlock)
{
Debug.Assert(childBlock.Children.Count > 0);
var child = childBlock.Children[0];
var childSpan = (Span)child;
// text tags that are labeled as transitions should be ignored aka they're not tag helpers.
return !string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) ||
childSpan.Kind != SpanKind.Transition;
}
private void TrackBlock(BlockBuilder builder)
{
_currentBlock = builder;
_blockStack.Push(builder);
}
private void TrackTagHelperBlock(TagHelperBlockBuilder builder)
{
_currentTagHelperTracker = new TagHelperBlockTracker(builder);
PushTrackerStack(_currentTagHelperTracker);
TrackBlock(builder);
}
private bool TryRecoverTagHelper(string tagName, Block endTag, ErrorSink errorSink)
{
var malformedTagHelperCount = 0;
foreach (var tracker in _trackerStack)
{
if (tracker.IsTagHelper && tracker.TagName.Equals(tagName, StringComparison.OrdinalIgnoreCase))
{
break;
}
malformedTagHelperCount++;
}
// If the malformedTagHelperCount == _tagStack.Count it means we couldn't find a start tag for the tag
// helper, can't recover.
if (malformedTagHelperCount != _trackerStack.Count)
{
BuildMalformedTagHelpers(malformedTagHelperCount, errorSink);
// One final build, this is the build that completes our target tag helper block which is not malformed.
BuildCurrentlyTrackedTagHelperBlock(endTag);
// We were able to recover
return true;
}
// Could not recover tag helper. Aka we found a tag helper end tag without a corresponding start tag.
return false;
}
private void BuildMalformedTagHelpers(int count, ErrorSink errorSink)
{
for (var i = 0; i < count; i++)
{
var tracker = _trackerStack.Peek();
// Skip all non-TagHelper entries. Non TagHelper trackers do not need to represent well-formed HTML.
if (!tracker.IsTagHelper)
{
PopTrackerStack();
continue;
}
var malformedTagHelper = ((TagHelperBlockTracker)tracker).Builder;
errorSink.OnError(
SourceLocation.Advance(malformedTagHelper.Start, "<"),
LegacyResources.FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper(
malformedTagHelper.TagName),
malformedTagHelper.TagName.Length);
BuildCurrentlyTrackedTagHelperBlock(endTag: null);
}
}
private static string GetTagName(Block tagBlock)
{
var child = tagBlock.Children[0];
if (tagBlock.Type != BlockType.Tag || tagBlock.Children.Count == 0 || !(child is Span))
{
return null;
}
var childSpan = (Span)child;
HtmlSymbol textSymbol = null;
for (var i = 0; i < childSpan.Symbols.Count; i++)
{
var symbol = childSpan.Symbols[i] as HtmlSymbol;
if (symbol != null &&
(symbol.Type & (HtmlSymbolType.WhiteSpace | HtmlSymbolType.Text)) == symbol.Type)
{
textSymbol = symbol;
break;
}
}
if (textSymbol == null)
{
return null;
}
return textSymbol.Type == HtmlSymbolType.WhiteSpace ? null : textSymbol.Content;
}
private static bool IsEndTag(Block tagBlock)
{
EnsureTagBlock(tagBlock);
var childSpan = (Span)tagBlock.Children.First();
// We grab the symbol that could be forward slash
var relevantSymbol = (HtmlSymbol)childSpan.Symbols[childSpan.Symbols.Count == 1 ? 0 : 1];
return relevantSymbol.Type == HtmlSymbolType.ForwardSlash;
}
private static void EnsureTagBlock(Block tagBlock)
{
Debug.Assert(tagBlock.Type == BlockType.Tag);
Debug.Assert(tagBlock.Children.First() is Span);
}
private static bool IsSelfClosing(Block childBlock)
{
var childSpan = childBlock.FindLastDescendentSpan();
return childSpan?.Content.EndsWith("/>", StringComparison.Ordinal) ?? false;
}
private void PushTrackerStack(TagBlockTracker tracker)
{
_currentParentTagName = tracker.TagName;
_trackerStack.Push(tracker);
}
private TagBlockTracker PopTrackerStack()
{
var poppedTracker = _trackerStack.Pop();
_currentParentTagName = _trackerStack.Count > 0 ? _trackerStack.Peek().TagName : null;
return poppedTracker;
}
private class TagBlockTracker
{
public TagBlockTracker(string tagName, bool isTagHelper, int depth)
{
TagName = tagName;
IsTagHelper = isTagHelper;
Depth = depth;
}
public string TagName { get; }
public bool IsTagHelper { get; }
public int Depth { get; }
}
private class TagHelperBlockTracker : TagBlockTracker
{
private IEnumerable<string> _prefixedAllowedChildren;
public TagHelperBlockTracker(TagHelperBlockBuilder builder)
: base(builder.TagName, isTagHelper: true, depth: 0)
{
Builder = builder;
if (Builder.Descriptors.Any(descriptor => descriptor.AllowedChildren != null))
{
AllowedChildren = Builder.Descriptors
.Where(descriptor => descriptor.AllowedChildren != null)
.SelectMany(descriptor => descriptor.AllowedChildren)
.Distinct(StringComparer.OrdinalIgnoreCase);
}
}
public TagHelperBlockBuilder Builder { get; }
public uint OpenMatchingTags { get; set; }
public IEnumerable<string> AllowedChildren { get; }
public IEnumerable<string> PrefixedAllowedChildren
{
get
{
if (AllowedChildren != null && _prefixedAllowedChildren == null)
{
Debug.Assert(Builder.Descriptors.Count() >= 1);
var prefix = Builder.Descriptors.First().Prefix;
_prefixedAllowedChildren = AllowedChildren.Select(allowedChild => prefix + allowedChild);
}
return _prefixedAllowedChildren;
}
}
}
}
}

View File

@ -0,0 +1,82 @@
// 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.Diagnostics;
using System.Linq;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// A metadata class describing a required tag helper attribute.
/// </summary>
internal class TagHelperRequiredAttributeDescriptor
{
/// <summary>
/// The HTML attribute name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The comparison method to use for <see cref="Name"/> when determining if an HTML attribute name matches.
/// </summary>
public TagHelperRequiredAttributeNameComparison NameComparison { get; set; }
/// <summary>
/// The HTML attribute value.
/// </summary>
public string Value { get; set; }
/// <summary>
/// The comparison method to use for <see cref="Value"/> when determining if an HTML attribute value matches.
/// </summary>
public TagHelperRequiredAttributeValueComparison ValueComparison { get; set; }
/// <summary>
/// Determines if the current <see cref="TagHelperRequiredAttributeDescriptor"/> matches the given
/// <paramref name="attributeName"/> and <paramref name="attributeValue"/>.
/// </summary>
/// <param name="attributeName">An HTML attribute name.</param>
/// <param name="attributeValue">An HTML attribute value.</param>
/// <returns><c>true</c> if the current <see cref="TagHelperRequiredAttributeDescriptor"/> matches
/// <paramref name="attributeName"/> and <paramref name="attributeValue"/>; <c>false</c> otherwise.</returns>
public bool IsMatch(string attributeName, string attributeValue)
{
var nameMatches = false;
if (NameComparison == TagHelperRequiredAttributeNameComparison.FullMatch)
{
nameMatches = string.Equals(Name, attributeName, StringComparison.OrdinalIgnoreCase);
}
else if (NameComparison == TagHelperRequiredAttributeNameComparison.PrefixMatch)
{
// attributeName cannot equal the Name if comparing as a PrefixMatch.
nameMatches = attributeName.Length != Name.Length &&
attributeName.StartsWith(Name, StringComparison.OrdinalIgnoreCase);
}
else
{
Debug.Assert(false, "Unknown name comparison.");
}
if (!nameMatches)
{
return false;
}
switch (ValueComparison)
{
case TagHelperRequiredAttributeValueComparison.None:
return true;
case TagHelperRequiredAttributeValueComparison.PrefixMatch: // Value starts with
return attributeValue.StartsWith(Value, StringComparison.Ordinal);
case TagHelperRequiredAttributeValueComparison.SuffixMatch: // Value ends with
return attributeValue.EndsWith(Value, StringComparison.Ordinal);
case TagHelperRequiredAttributeValueComparison.FullMatch: // Value equals
return string.Equals(attributeValue, Value, StringComparison.Ordinal);
default:
Debug.Assert(false, "Unknown value comparison.");
return false;
}
}
}
}

View File

@ -0,0 +1,58 @@
// 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;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// An <see cref="IEqualityComparer{TagHelperRequiredAttributeDescriptor}"/> used to check equality between
/// two <see cref="TagHelperRequiredAttributeDescriptor"/>s.
/// </summary>
internal class TagHelperRequiredAttributeDescriptorComparer : IEqualityComparer<TagHelperRequiredAttributeDescriptor>
{
/// <summary>
/// A default instance of the <see cref="TagHelperRequiredAttributeDescriptor"/>.
/// </summary>
public static readonly TagHelperRequiredAttributeDescriptorComparer Default =
new TagHelperRequiredAttributeDescriptorComparer();
/// <summary>
/// Initializes a new <see cref="TagHelperRequiredAttributeDescriptor"/> instance.
/// </summary>
protected TagHelperRequiredAttributeDescriptorComparer()
{
}
/// <inheritdoc />
public virtual bool Equals(
TagHelperRequiredAttributeDescriptor descriptorX,
TagHelperRequiredAttributeDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}
return descriptorX != null &&
descriptorX.NameComparison == descriptorY.NameComparison &&
descriptorX.ValueComparison == descriptorY.ValueComparison &&
string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.OrdinalIgnoreCase) &&
string.Equals(descriptorX.Value, descriptorY.Value, StringComparison.Ordinal);
}
/// <inheritdoc />
public virtual int GetHashCode(TagHelperRequiredAttributeDescriptor descriptor)
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(descriptor.NameComparison);
hashCodeCombiner.Add(descriptor.ValueComparison);
hashCodeCombiner.Add(descriptor.Name, StringComparer.OrdinalIgnoreCase);
hashCodeCombiner.Add(descriptor.Value, StringComparer.Ordinal);
return hashCodeCombiner.CombinedHash;
}
}
}

View File

@ -0,0 +1,21 @@
// 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.
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// Acceptable <see cref="TagHelperRequiredAttributeDescriptor.Name"/> comparison modes.
/// </summary>
internal enum TagHelperRequiredAttributeNameComparison
{
/// <summary>
/// HTML attribute name case insensitively matches <see cref="TagHelperRequiredAttributeDescriptor.Name"/>.
/// </summary>
FullMatch,
/// <summary>
/// HTML attribute name case insensitively starts with <see cref="TagHelperRequiredAttributeDescriptor.Name"/>.
/// </summary>
PrefixMatch,
}
}

View File

@ -0,0 +1,31 @@
// 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.
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// Acceptable <see cref="TagHelperRequiredAttributeDescriptor.Value"/> comparison modes.
/// </summary>
internal enum TagHelperRequiredAttributeValueComparison
{
/// <summary>
/// HTML attribute value always matches <see cref="TagHelperRequiredAttributeDescriptor.Value"/>.
/// </summary>
None,
/// <summary>
/// HTML attribute value case sensitively matches <see cref="TagHelperRequiredAttributeDescriptor.Value"/>.
/// </summary>
FullMatch,
/// <summary>
/// HTML attribute value case sensitively starts with <see cref="TagHelperRequiredAttributeDescriptor.Value"/>.
/// </summary>
PrefixMatch,
/// <summary>
/// HTML attribute value case sensitively ends with <see cref="TagHelperRequiredAttributeDescriptor.Value"/>.
/// </summary>
SuffixMatch,
}
}

View File

@ -0,0 +1,26 @@
// 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.
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// The mode in which an element should render.
/// </summary>
internal enum TagMode
{
/// <summary>
/// Include both start and end tags.
/// </summary>
StartTagAndEndTag,
/// <summary>
/// A self-closed tag.
/// </summary>
SelfClosing,
/// <summary>
/// Only a start tag.
/// </summary>
StartTagOnly
}
}

View File

@ -0,0 +1,28 @@
// 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.
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// The structure the element should be written in.
/// </summary>
internal enum TagStructure
{
/// <summary>
/// If no other tag helper applies to the same element and specifies a <see cref="TagStructure"/>,
/// <see cref="NormalOrSelfClosing"/> will be used.
/// </summary>
Unspecified,
/// <summary>
/// Element can be written as &lt;my-tag-helper&gt;&lt;/my-tag-helper&gt; or &lt;my-tag-helper /&gt;.
/// </summary>
NormalOrSelfClosing,
/// <summary>
/// Element can be written as &lt;my-tag-helper&gt; or &lt;my-tag-helper /&gt;.
/// </summary>
/// <remarks>Elements with a <see cref="WithoutEndTag"/> structure will never have any content.</remarks>
WithoutEndTag
}
}

View File

@ -0,0 +1,64 @@
// 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;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
/// <summary>
/// An <see cref="IEqualityComparer{TagHelperDescriptor}"/> that checks equality between two
/// <see cref="TagHelperDescriptor"/>s using only their <see cref="TagHelperDescriptor.AssemblyName"/>s and
/// <see cref="TagHelperDescriptor.TypeName"/>s.
/// </summary>
/// <remarks>
/// This class is intended for scenarios where Reflection-based information is all important i.e.
/// <see cref="TagHelperDescriptor.RequiredAttributes"/>, <see cref="TagHelperDescriptor.TagName"/>, and related
/// properties are not relevant.
/// </remarks>
internal class TypeBasedTagHelperDescriptorComparer : IEqualityComparer<TagHelperDescriptor>
{
/// <summary>
/// A default instance of the <see cref="TypeBasedTagHelperDescriptorComparer"/>.
/// </summary>
public static readonly TypeBasedTagHelperDescriptorComparer Default =
new TypeBasedTagHelperDescriptorComparer();
private TypeBasedTagHelperDescriptorComparer()
{
}
/// <inheritdoc />
/// <remarks>
/// Determines equality based on <see cref="TagHelperDescriptor.AssemblyName"/> and
/// <see cref="TagHelperDescriptor.TypeName"/>.
/// </remarks>
public bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}
return descriptorX != null &&
string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal) &&
string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal);
}
/// <inheritdoc />
public int GetHashCode(TagHelperDescriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(descriptor.AssemblyName, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.TypeName, StringComparer.Ordinal);
return hashCodeCombiner;
}
}
}

View File

@ -335,6 +335,9 @@ Instead, wrap the contents of the block in "{{}}":
<data name="Parser_Context_Not_Set" xml:space="preserve">
<value>Parser was started with a null Context property. The Context property must be set BEFORE calling any methods on the parser.</value>
</data>
<data name="RewriterError_EmptyTagHelperBoundAttribute" xml:space="preserve">
<value>Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound attributes of type '{2}' cannot be empty or contain only whitespace.</value>
</data>
<data name="SectionExample_CS" xml:space="preserve">
<value>@section Header { ... }</value>
<comment>In CSHTML, the @section keyword is case-sensitive and lowercase (as with all C# keywords)</comment>
@ -345,6 +348,36 @@ Instead, wrap the contents of the block in "{{}}":
<data name="Symbol_Unknown" xml:space="preserve">
<value>&lt;&lt;unknown&gt;&gt;</value>
</data>
<data name="TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey" xml:space="preserve">
<value>The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '&lt;{1} {0}{{ key }}="value"&gt;'.</value>
</data>
<data name="TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed" xml:space="preserve">
<value>TagHelper attributes must be well-formed.</value>
</data>
<data name="TagHelperParseTreeRewriter_CannotHaveNonTagContent" xml:space="preserve">
<value>The parent &lt;{0}&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_EndTagTagHelperMustNotHaveAnEndTag" xml:space="preserve">
<value>Found an end tag (&lt;/{0}&gt;) for tag helper '{1}' with tag structure that disallows an end tag ('{2}').</value>
</data>
<data name="TagHelperParseTreeRewriter_InconsistentTagStructure" xml:space="preserve">
<value>Tag helpers '{0}' and '{1}' targeting element '{2}' must not expect different {3} values.</value>
</data>
<data name="TagHelperParseTreeRewriter_InvalidNestedTag" xml:space="preserve">
<value>The &lt;{0}&gt; tag is not allowed by parent &lt;{1}&gt; tag helper. Only child tags with name(s) '{2}' are allowed.</value>
</data>
<data name="TagHelpersParseTreeRewriter_FoundMalformedTagHelper" xml:space="preserve">
<value>Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing.</value>
</data>
<data name="TagHelpersParseTreeRewriter_MissingCloseAngle" xml:space="preserve">
<value>Missing close angle for tag helper '{0}'.</value>
</data>
<data name="TagHelpers_AttributesMustHaveAName" xml:space="preserve">
<value>Tag Helper '{0}'s attributes must have names.</value>
</data>
<data name="TagHelpers_CannotHaveCSharpInTagDeclaration" xml:space="preserve">
<value>The tag helper '{0}' must not have C# in the element's attribute declaration area.</value>
</data>
<data name="TokenizerView_CannotPutBack" xml:space="preserve">
<value>In order to put a symbol back, it must have been the symbol which ended at the current position. The specified symbol ends at {0}, but the current position is {1}</value>
</data>

View File

@ -1078,6 +1078,22 @@ namespace Microsoft.AspNetCore.Razor.Evolution
return GetString("Parser_Context_Not_Set");
}
/// <summary>
/// Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound attributes of type '{2}' cannot be empty or contain only whitespace.
/// </summary>
internal static string RewriterError_EmptyTagHelperBoundAttribute
{
get { return GetString("RewriterError_EmptyTagHelperBoundAttribute"); }
}
/// <summary>
/// Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound attributes of type '{2}' cannot be empty or contain only whitespace.
/// </summary>
internal static string FormatRewriterError_EmptyTagHelperBoundAttribute(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RewriterError_EmptyTagHelperBoundAttribute"), p0, p1, p2);
}
/// <summary>
/// @section Header { ... }
/// </summary>
@ -1126,6 +1142,166 @@ namespace Microsoft.AspNetCore.Razor.Evolution
return GetString("Symbol_Unknown");
}
/// <summary>
/// The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '&lt;{1} {0}{{ key }}="value"&gt;'.
/// </summary>
internal static string TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey
{
get { return GetString("TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey"); }
}
/// <summary>
/// The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '&lt;{1} {0}{{ key }}="value"&gt;'.
/// </summary>
internal static string FormatTagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey"), p0, p1);
}
/// <summary>
/// TagHelper attributes must be well-formed.
/// </summary>
internal static string TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed
{
get { return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed"); }
}
/// <summary>
/// TagHelper attributes must be well-formed.
/// </summary>
internal static string FormatTagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed()
{
return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed");
}
/// <summary>
/// The parent &lt;{0}&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 &lt;{0}&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>
/// Found an end tag (&lt;/{0}&gt;) for tag helper '{1}' with tag structure that disallows an end tag ('{2}').
/// </summary>
internal static string TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag
{
get { return GetString("TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag"); }
}
/// <summary>
/// Found an end tag (&lt;/{0}&gt;) for tag helper '{1}' with tag structure that disallows an end tag ('{2}').
/// </summary>
internal static string FormatTagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag"), p0, p1, p2);
}
/// <summary>
/// Tag helpers '{0}' and '{1}' targeting element '{2}' must not expect different {3} values.
/// </summary>
internal static string TagHelperParseTreeRewriter_InconsistentTagStructure
{
get { return GetString("TagHelperParseTreeRewriter_InconsistentTagStructure"); }
}
/// <summary>
/// Tag helpers '{0}' and '{1}' targeting element '{2}' must not expect different {3} values.
/// </summary>
internal static string FormatTagHelperParseTreeRewriter_InconsistentTagStructure(object p0, object p1, object p2, object p3)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_InconsistentTagStructure"), p0, p1, p2, p3);
}
/// <summary>
/// The &lt;{0}&gt; tag is not allowed by parent &lt;{1}&gt; tag helper. Only child tags with name(s) '{2}' are allowed.
/// </summary>
internal static string TagHelperParseTreeRewriter_InvalidNestedTag
{
get { return GetString("TagHelperParseTreeRewriter_InvalidNestedTag"); }
}
/// <summary>
/// The &lt;{0}&gt; tag is not allowed by parent &lt;{1}&gt; tag helper. Only child tags with 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);
}
/// <summary>
/// Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing.
/// </summary>
internal static string TagHelpersParseTreeRewriter_FoundMalformedTagHelper
{
get { return GetString("TagHelpersParseTreeRewriter_FoundMalformedTagHelper"); }
}
/// <summary>
/// Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing.
/// </summary>
internal static string FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpersParseTreeRewriter_FoundMalformedTagHelper"), p0);
}
/// <summary>
/// Missing close angle for tag helper '{0}'.
/// </summary>
internal static string TagHelpersParseTreeRewriter_MissingCloseAngle
{
get { return GetString("TagHelpersParseTreeRewriter_MissingCloseAngle"); }
}
/// <summary>
/// Missing close angle for tag helper '{0}'.
/// </summary>
internal static string FormatTagHelpersParseTreeRewriter_MissingCloseAngle(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpersParseTreeRewriter_MissingCloseAngle"), p0);
}
/// <summary>
/// Tag Helper '{0}'s attributes must have names.
/// </summary>
internal static string TagHelpers_AttributesMustHaveAName
{
get { return GetString("TagHelpers_AttributesMustHaveAName"); }
}
/// <summary>
/// Tag Helper '{0}'s attributes must have names.
/// </summary>
internal static string FormatTagHelpers_AttributesMustHaveAName(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_AttributesMustHaveAName"), p0);
}
/// <summary>
/// The tag helper '{0}' must not have C# in the element's attribute declaration area.
/// </summary>
internal static string TagHelpers_CannotHaveCSharpInTagDeclaration
{
get { return GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"); }
}
/// <summary>
/// The tag helper '{0}' must not have C# in the element's attribute declaration area.
/// </summary>
internal static string FormatTagHelpers_CannotHaveCSharpInTagDeclaration(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"), p0);
}
/// <summary>
/// In order to put a symbol back, it must have been the symbol which ended at the current position. The specified symbol ends at {0}, but the current position is {1}
/// </summary>

View File

@ -54,5 +54,27 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
_factory.Markup(content).Accepts(acceptedCharacters)
);
}
public Block TagHelperBlock(
string tagName,
TagMode tagMode,
SourceLocation start,
Block startTag,
SyntaxTreeNode[] children,
Block endTag)
{
var builder = new TagHelperBlockBuilder(
tagName,
tagMode,
attributes: new List<TagHelperAttributeNode>(),
children: children)
{
Start = start,
SourceStartTag = startTag,
SourceEndTag = endTag
};
return builder.Build();
}
}
}

View File

@ -125,6 +125,69 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
}
internal class MarkupTagHelperBlock : TagHelperBlock
{
public MarkupTagHelperBlock(string tagName)
: this(tagName, tagMode: TagMode.StartTagAndEndTag, attributes: new List<TagHelperAttributeNode>())
{
}
public MarkupTagHelperBlock(string tagName, TagMode tagMode)
: this(tagName, tagMode, new List<TagHelperAttributeNode>())
{
}
public MarkupTagHelperBlock(
string tagName,
IList<TagHelperAttributeNode> attributes)
: this(tagName, TagMode.StartTagAndEndTag, attributes, children: new SyntaxTreeNode[0])
{
}
public MarkupTagHelperBlock(
string tagName,
TagMode tagMode,
IList<TagHelperAttributeNode> attributes)
: this(tagName, tagMode, attributes, new SyntaxTreeNode[0])
{
}
public MarkupTagHelperBlock(string tagName, params SyntaxTreeNode[] children)
: this(
tagName,
TagMode.StartTagAndEndTag,
attributes: new List<TagHelperAttributeNode>(),
children: children)
{
}
public MarkupTagHelperBlock(string tagName, TagMode tagMode, params SyntaxTreeNode[] children)
: this(tagName, tagMode, new List<TagHelperAttributeNode>(), children)
{
}
public MarkupTagHelperBlock(
string tagName,
IList<TagHelperAttributeNode> attributes,
params SyntaxTreeNode[] children)
: base(new TagHelperBlockBuilder(
tagName,
TagMode.StartTagAndEndTag,
attributes: attributes,
children: children))
{
}
public MarkupTagHelperBlock(
string tagName,
TagMode tagMode,
IList<TagHelperAttributeNode> attributes,
params SyntaxTreeNode[] children)
: base(new TagHelperBlockBuilder(tagName, tagMode, attributes, children))
{
}
}
internal class SectionBlock : Block
{
private const BlockType ThisBlockType = BlockType.Section;

View File

@ -0,0 +1,95 @@
// 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.Linq;
using Microsoft.Extensions.Internal;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal class CaseSensitiveTagHelperDescriptorComparer : TagHelperDescriptorComparer
{
public new static readonly CaseSensitiveTagHelperDescriptorComparer Default =
new CaseSensitiveTagHelperDescriptorComparer();
private CaseSensitiveTagHelperDescriptorComparer()
: base()
{
}
public override bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}
Assert.True(base.Equals(descriptorX, descriptorY));
// Normal comparer doesn't care about the case, required attribute order, allowed children order,
// attributes or prefixes. In tests we do.
Assert.Equal(descriptorX.TagName, descriptorY.TagName, StringComparer.Ordinal);
Assert.Equal(descriptorX.Prefix, descriptorY.Prefix, StringComparer.Ordinal);
Assert.Equal(
descriptorX.RequiredAttributes,
descriptorY.RequiredAttributes,
CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.Default);
Assert.Equal(descriptorX.RequiredParent, descriptorY.RequiredParent, StringComparer.Ordinal);
if (descriptorX.AllowedChildren != descriptorY.AllowedChildren)
{
Assert.Equal(descriptorX.AllowedChildren, descriptorY.AllowedChildren, StringComparer.Ordinal);
}
Assert.Equal(
descriptorX.Attributes,
descriptorY.Attributes,
TagHelperAttributeDescriptorComparer.Default);
Assert.Equal(
descriptorX.DesignTimeDescriptor,
descriptorY.DesignTimeDescriptor,
TagHelperDesignTimeDescriptorComparer.Default);
return true;
}
public override int GetHashCode(TagHelperDescriptor descriptor)
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(base.GetHashCode(descriptor));
hashCodeCombiner.Add(descriptor.TagName, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.Prefix, StringComparer.Ordinal);
if (descriptor.DesignTimeDescriptor != null)
{
hashCodeCombiner.Add(
TagHelperDesignTimeDescriptorComparer.Default.GetHashCode(descriptor.DesignTimeDescriptor));
}
foreach (var requiredAttribute in descriptor.RequiredAttributes.OrderBy(attribute => attribute.Name))
{
hashCodeCombiner.Add(
CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.Default.GetHashCode(requiredAttribute));
}
if (descriptor.AllowedChildren != null)
{
foreach (var child in descriptor.AllowedChildren.OrderBy(child => child))
{
hashCodeCombiner.Add(child, StringComparer.Ordinal);
}
}
var orderedAttributeHashCodes = descriptor.Attributes
.Select(attribute => TagHelperAttributeDescriptorComparer.Default.GetHashCode(attribute))
.OrderBy(hashcode => hashcode);
foreach (var attributeHashCode in orderedAttributeHashCodes)
{
hashCodeCombiner.Add(attributeHashCode);
}
return hashCodeCombiner.CombinedHash;
}
}
}

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 Microsoft.Extensions.Internal;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal class CaseSensitiveTagHelperRequiredAttributeDescriptorComparer : TagHelperRequiredAttributeDescriptorComparer
{
public new static readonly CaseSensitiveTagHelperRequiredAttributeDescriptorComparer Default =
new CaseSensitiveTagHelperRequiredAttributeDescriptorComparer();
private CaseSensitiveTagHelperRequiredAttributeDescriptorComparer()
: base()
{
}
public override bool Equals(TagHelperRequiredAttributeDescriptor descriptorX, TagHelperRequiredAttributeDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}
Assert.True(base.Equals(descriptorX, descriptorY));
Assert.Equal(descriptorX.Name, descriptorY.Name, StringComparer.Ordinal);
return true;
}
public override int GetHashCode(TagHelperRequiredAttributeDescriptor descriptor)
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(base.GetHashCode(descriptor));
hashCodeCombiner.Add(descriptor.Name, StringComparer.Ordinal);
return hashCodeCombiner.CombinedHash;
}
}
}

View File

@ -282,6 +282,35 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
}
private static void EvaluateTagHelperAttribute(
ErrorCollector collector,
TagHelperAttributeNode actual,
TagHelperAttributeNode expected)
{
if (actual.Name != expected.Name)
{
collector.AddError("{0} - FAILED :: Attribute names do not match", expected.Name);
}
else
{
collector.AddMessage("{0} - PASSED :: Attribute names match", expected.Name);
}
if (actual.ValueStyle != expected.ValueStyle)
{
collector.AddError("{0} - FAILED :: Attribute value styles do not match", expected.ValueStyle.ToString());
}
else
{
collector.AddMessage("{0} - PASSED :: Attribute value style match", expected.ValueStyle);
}
if (actual.ValueStyle != HtmlAttributeValueStyle.Minimized)
{
EvaluateSyntaxTreeNode(collector, actual.Value, expected.Value);
}
}
private static void EvaluateSyntaxTreeNode(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected)
{
if (actual == null)
@ -327,6 +356,11 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
else
{
if (actual is TagHelperBlock)
{
EvaluateTagHelperBlock(collector, actual as TagHelperBlock, expected as TagHelperBlock);
}
AddPassedMessage(collector, expected);
using (collector.Indent())
{
@ -351,6 +385,50 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
}
private static void EvaluateTagHelperBlock(ErrorCollector collector, TagHelperBlock actual, TagHelperBlock expected)
{
if (expected == null)
{
AddMismatchError(collector, actual, expected);
}
else
{
if (!string.Equals(expected.TagName, actual.TagName, StringComparison.Ordinal))
{
collector.AddError(
"{0} - FAILED :: TagName mismatch for TagHelperBlock :: ACTUAL: {1}",
expected.TagName,
actual.TagName);
}
if (expected.TagMode != actual.TagMode)
{
collector.AddError(
$"{expected.TagMode} - FAILED :: {nameof(TagMode)} for {nameof(TagHelperBlock)} " +
$"{actual.TagName} :: ACTUAL: {actual.TagMode}");
}
var expectedAttributes = expected.Attributes.GetEnumerator();
var actualAttributes = actual.Attributes.GetEnumerator();
while (expectedAttributes.MoveNext())
{
if (!actualAttributes.MoveNext())
{
collector.AddError("{0} - FAILED :: No more attributes on this node", expectedAttributes.Current);
}
else
{
EvaluateTagHelperAttribute(collector, actualAttributes.Current, expectedAttributes.Current);
}
}
while (actualAttributes.MoveNext())
{
collector.AddError("End of Attributes - FAILED :: Found Attribute: {0}", actualAttributes.Current.Name);
}
}
}
private static void AddPassedMessage(ErrorCollector collector, SyntaxTreeNode expected)
{
collector.AddMessage("{0} - PASSED", expected);

View File

@ -0,0 +1,56 @@
// 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;
using Microsoft.Extensions.Internal;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal class TagHelperAttributeDescriptorComparer : IEqualityComparer<TagHelperAttributeDescriptor>
{
public static readonly TagHelperAttributeDescriptorComparer Default =
new TagHelperAttributeDescriptorComparer();
private TagHelperAttributeDescriptorComparer()
{
}
public bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}
Assert.NotNull(descriptorX);
Assert.NotNull(descriptorY);
Assert.Equal(descriptorX.IsIndexer, descriptorY.IsIndexer);
Assert.Equal(descriptorX.Name, descriptorY.Name, StringComparer.Ordinal);
Assert.Equal(descriptorX.PropertyName, descriptorY.PropertyName, StringComparer.Ordinal);
Assert.Equal(descriptorX.TypeName, descriptorY.TypeName, StringComparer.Ordinal);
Assert.Equal(descriptorX.IsEnum, descriptorY.IsEnum);
Assert.Equal(descriptorX.IsStringProperty, descriptorY.IsStringProperty);
return TagHelperAttributeDesignTimeDescriptorComparer.Default.Equals(
descriptorX.DesignTimeDescriptor,
descriptorY.DesignTimeDescriptor);
}
public int GetHashCode(TagHelperAttributeDescriptor descriptor)
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(descriptor.IsIndexer);
hashCodeCombiner.Add(descriptor.Name, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.PropertyName, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.TypeName, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.IsEnum);
hashCodeCombiner.Add(descriptor.IsStringProperty);
hashCodeCombiner.Add(TagHelperAttributeDesignTimeDescriptorComparer.Default.GetHashCode(
descriptor.DesignTimeDescriptor));
return hashCodeCombiner;
}
}
}

View File

@ -0,0 +1,47 @@
// 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;
using Microsoft.Extensions.Internal;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal class TagHelperAttributeDesignTimeDescriptorComparer :
IEqualityComparer<TagHelperAttributeDesignTimeDescriptor>
{
public static readonly TagHelperAttributeDesignTimeDescriptorComparer Default =
new TagHelperAttributeDesignTimeDescriptorComparer();
private TagHelperAttributeDesignTimeDescriptorComparer()
{
}
public bool Equals(
TagHelperAttributeDesignTimeDescriptor descriptorX,
TagHelperAttributeDesignTimeDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}
Assert.NotNull(descriptorX);
Assert.NotNull(descriptorY);
Assert.Equal(descriptorX.Summary, descriptorY.Summary, StringComparer.Ordinal);
Assert.Equal(descriptorX.Remarks, descriptorY.Remarks, StringComparer.Ordinal);
return true;
}
public int GetHashCode(TagHelperAttributeDesignTimeDescriptor descriptor)
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(descriptor.Summary, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.Remarks, StringComparer.Ordinal);
return hashCodeCombiner;
}
}
}

View File

@ -0,0 +1,105 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
public class TagHelperBlockTest
{
[Fact]
public void FlattenFlattensSelfClosingTagHelpers()
{
// Arrange
var spanFactory = new SpanFactory();
var blockFactory = new BlockFactory(spanFactory);
var tagHelper = (TagHelperBlock)blockFactory.TagHelperBlock(
tagName: "input",
tagMode: TagMode.SelfClosing,
start: SourceLocation.Zero,
startTag: blockFactory.MarkupTagBlock("<input />"),
children: new SyntaxTreeNode[0],
endTag: null);
spanFactory.Reset();
var expectedNode = spanFactory.Markup("<input />");
// Act
var flattenedNodes = tagHelper.Flatten();
// Assert
var node = Assert.Single(flattenedNodes);
Assert.True(node.EquivalentTo(expectedNode));
}
[Fact]
public void FlattenFlattensStartAndEndTagTagHelpers()
{
// Arrange
var spanFactory = new SpanFactory();
var blockFactory = new BlockFactory(spanFactory);
var tagHelper = (TagHelperBlock)blockFactory.TagHelperBlock(
tagName: "div",
tagMode: TagMode.StartTagAndEndTag,
start: SourceLocation.Zero,
startTag: blockFactory.MarkupTagBlock("<div>"),
children: new SyntaxTreeNode[0],
endTag: blockFactory.MarkupTagBlock("</div>"));
spanFactory.Reset();
var expectedStartTag = spanFactory.Markup("<div>");
var expectedEndTag = spanFactory.Markup("</div>");
// Act
var flattenedNodes = tagHelper.Flatten();
// Assert
Assert.Collection(
flattenedNodes,
first =>
{
Assert.True(first.EquivalentTo(expectedStartTag));
},
second =>
{
Assert.True(second.EquivalentTo(expectedEndTag));
});
}
[Fact]
public void FlattenFlattensStartAndEndTagWithChildrenTagHelpers()
{
// Arrange
var spanFactory = new SpanFactory();
var blockFactory = new BlockFactory(spanFactory);
var tagHelper = (TagHelperBlock)blockFactory.TagHelperBlock(
tagName: "div",
tagMode: TagMode.StartTagAndEndTag,
start: SourceLocation.Zero,
startTag: blockFactory.MarkupTagBlock("<div>"),
children: new SyntaxTreeNode[] { spanFactory.Markup("Hello World") },
endTag: blockFactory.MarkupTagBlock("</div>"));
spanFactory.Reset();
var expectedStartTag = spanFactory.Markup("<div>");
var expectedChildren = spanFactory.Markup("Hello World");
var expectedEndTag = spanFactory.Markup("</div>");
// Act
var flattenedNodes = tagHelper.Flatten();
// Assert
Assert.Collection(
flattenedNodes,
first =>
{
Assert.True(first.EquivalentTo(expectedStartTag));
},
second =>
{
Assert.True(second.EquivalentTo(expectedChildren));
},
third =>
{
Assert.True(third.EquivalentTo(expectedEndTag));
});
}
}
}

View File

@ -0,0 +1,498 @@
// 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;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
public class TagHelperDescriptorProviderTest
{
public static TheoryData RequiredParentData
{
get
{
var strongPParent = new TagHelperDescriptor
{
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "p",
};
var strongDivParent = new TagHelperDescriptor
{
TagName = "strong",
TypeName = "StrongTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "div",
};
var catchAllPParent = new TagHelperDescriptor
{
TagName = "*",
TypeName = "CatchAllTagHelper",
AssemblyName = "SomeAssembly",
RequiredParent = "p",
};
return new TheoryData<
string, // tagName
string, // parentTagName
IEnumerable<TagHelperDescriptor>, // availableDescriptors
IEnumerable<TagHelperDescriptor>> // expectedDescriptors
{
{
"strong",
"p",
new[] { strongPParent, strongDivParent },
new[] { strongPParent }
},
{
"strong",
"div",
new[] { strongPParent, strongDivParent, catchAllPParent },
new[] { strongDivParent }
},
{
"strong",
"p",
new[] { strongPParent, strongDivParent, catchAllPParent },
new[] { strongPParent, catchAllPParent }
},
{
"custom",
"p",
new[] { strongPParent, strongDivParent, catchAllPParent },
new[] { catchAllPParent }
},
};
}
}
[Theory]
[MemberData(nameof(RequiredParentData))]
public void GetDescriptors_ReturnsDescriptorsParentTags(
string tagName,
string parentTagName,
object availableDescriptors,
object expectedDescriptors)
{
// Arrange
var provider = new TagHelperDescriptorProvider((IEnumerable<TagHelperDescriptor>)availableDescriptors);
// Act
var resolvedDescriptors = provider.GetDescriptors(
tagName,
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: parentTagName);
// Assert
Assert.Equal((IEnumerable<TagHelperDescriptor>)expectedDescriptors, resolvedDescriptors, CaseSensitiveTagHelperDescriptorComparer.Default);
}
public static TheoryData RequiredAttributeData
{
get
{
var divDescriptor = new TagHelperDescriptor
{
TagName = "div",
TypeName = "DivTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "style" } }
};
var inputDescriptor = new TagHelperDescriptor
{
TagName = "input",
TypeName = "InputTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "class" },
new TagHelperRequiredAttributeDescriptor { Name = "style" }
}
};
var inputWildcardPrefixDescriptor = new TagHelperDescriptor
{
TagName = "input",
TypeName = "InputWildCardAttribute",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "nodashprefix",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
}
}
};
var catchAllDescriptor = new TagHelperDescriptor
{
TagName = TagHelperDescriptorProvider.ElementCatchAllTarget,
TypeName = "CatchAllTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } }
};
var catchAllDescriptor2 = new TagHelperDescriptor
{
TagName = TagHelperDescriptorProvider.ElementCatchAllTarget,
TypeName = "CatchAllTagHelper",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor { Name = "custom" },
new TagHelperRequiredAttributeDescriptor { Name = "class" }
}
};
var catchAllWildcardPrefixDescriptor = new TagHelperDescriptor
{
TagName = TagHelperDescriptorProvider.ElementCatchAllTarget,
TypeName = "CatchAllWildCardAttribute",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "prefix-",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
}
}
};
var defaultAvailableDescriptors =
new[] { divDescriptor, inputDescriptor, catchAllDescriptor, catchAllDescriptor2 };
var defaultWildcardDescriptors =
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor };
Func<string, KeyValuePair<string, string>> kvp =
(name) => new KeyValuePair<string, string>(name, "test value");
return new TheoryData<
string, // tagName
IEnumerable<KeyValuePair<string, string>>, // providedAttributes
IEnumerable<TagHelperDescriptor>, // availableDescriptors
IEnumerable<TagHelperDescriptor>> // expectedDescriptors
{
{
"div",
new[] { kvp("custom") },
defaultAvailableDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{ "div", new[] { kvp("style") }, defaultAvailableDescriptors, new[] { divDescriptor } },
{ "div", new[] { kvp("class") }, defaultAvailableDescriptors, new[] { catchAllDescriptor } },
{
"div",
new[] { kvp("class"), kvp("style") },
defaultAvailableDescriptors,
new[] { divDescriptor, catchAllDescriptor }
},
{
"div",
new[] { kvp("class"), kvp("style"), kvp("custom") },
defaultAvailableDescriptors,
new[] { divDescriptor, catchAllDescriptor, catchAllDescriptor2 }
},
{
"input",
new[] { kvp("class"), kvp("style") },
defaultAvailableDescriptors,
new[] { inputDescriptor, catchAllDescriptor }
},
{
"input",
new[] { kvp("nodashprefixA") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor }
},
{
"input",
new[] { kvp("nodashprefix-ABC-DEF"), kvp("random") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor }
},
{
"input",
new[] { kvp("prefixABCnodashprefix") },
defaultWildcardDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{
"input",
new[] { kvp("prefix-") },
defaultWildcardDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{
"input",
new[] { kvp("nodashprefix") },
defaultWildcardDescriptors,
Enumerable.Empty<TagHelperDescriptor>()
},
{
"input",
new[] { kvp("prefix-A") },
defaultWildcardDescriptors,
new[] { catchAllWildcardPrefixDescriptor }
},
{
"input",
new[] { kvp("prefix-ABC-DEF"), kvp("random") },
defaultWildcardDescriptors,
new[] { catchAllWildcardPrefixDescriptor }
},
{
"input",
new[] { kvp("prefix-abc"), kvp("nodashprefix-def") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor }
},
{
"input",
new[] { kvp("class"), kvp("prefix-abc"), kvp("onclick"), kvp("nodashprefix-def"), kvp("style") },
defaultWildcardDescriptors,
new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor }
},
};
}
}
[Theory]
[MemberData(nameof(RequiredAttributeData))]
public void GetDescriptors_ReturnsDescriptorsWithRequiredAttributes(
string tagName,
IEnumerable<KeyValuePair<string, string>> providedAttributes,
object availableDescriptors,
object expectedDescriptors)
{
// Arrange
var provider = new TagHelperDescriptorProvider((IEnumerable<TagHelperDescriptor>)availableDescriptors);
// Act
var resolvedDescriptors = provider.GetDescriptors(tagName, providedAttributes, parentTagName: "p");
// Assert
Assert.Equal((IEnumerable<TagHelperDescriptor>)expectedDescriptors, resolvedDescriptors, CaseSensitiveTagHelperDescriptorComparer.Default);
}
[Fact]
public void GetDescriptors_ReturnsEmptyDescriptorsWithPrefixAsTagName()
{
// Arrange
var catchAllDescriptor = CreatePrefixedDescriptor(
"th",
TagHelperDescriptorProvider.ElementCatchAllTarget,
"foo1");
var descriptors = new[] { catchAllDescriptor };
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var resolvedDescriptors = provider.GetDescriptors(
tagName: "th",
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
Assert.Empty(resolvedDescriptors);
}
[Fact]
public void GetDescriptors_OnlyUnderstandsSinglePrefix()
{
// Arrange
var divDescriptor = CreatePrefixedDescriptor("th:", "div", "foo1");
var spanDescriptor = CreatePrefixedDescriptor("th2:", "span", "foo2");
var descriptors = new[] { divDescriptor, spanDescriptor };
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "th:div",
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
var retrievedDescriptorsSpan = provider.GetDescriptors(
tagName: "th2:span",
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
var descriptor = Assert.Single(retrievedDescriptorsDiv);
Assert.Same(divDescriptor, descriptor);
Assert.Empty(retrievedDescriptorsSpan);
}
[Fact]
public void GetDescriptors_ReturnsCatchAllDescriptorsForPrefixedTags()
{
// Arrange
var catchAllDescriptor = CreatePrefixedDescriptor("th:", TagHelperDescriptorProvider.ElementCatchAllTarget, "foo1");
var descriptors = new[] { catchAllDescriptor };
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "th:div",
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
var retrievedDescriptorsSpan = provider.GetDescriptors(
tagName: "th:span",
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
var descriptor = Assert.Single(retrievedDescriptorsDiv);
Assert.Same(catchAllDescriptor, descriptor);
descriptor = Assert.Single(retrievedDescriptorsSpan);
Assert.Same(catchAllDescriptor, descriptor);
}
[Fact]
public void GetDescriptors_ReturnsDescriptorsForPrefixedTags()
{
// Arrange
var divDescriptor = CreatePrefixedDescriptor("th:", "div", "foo1");
var descriptors = new[] { divDescriptor };
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptors = provider.GetDescriptors(
tagName: "th:div",
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
var descriptor = Assert.Single(retrievedDescriptors);
Assert.Same(divDescriptor, descriptor);
}
[Theory]
[InlineData("*")]
[InlineData("div")]
public void GetDescriptors_ReturnsNothingForUnprefixedTags(string tagName)
{
// Arrange
var divDescriptor = CreatePrefixedDescriptor("th:", tagName, "foo1");
var descriptors = new[] { divDescriptor };
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptorsDiv = provider.GetDescriptors(
tagName: "div",
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
Assert.Empty(retrievedDescriptorsDiv);
}
[Fact]
public void GetDescriptors_ReturnsNothingForUnregisteredTags()
{
// Arrange
var divDescriptor = new TagHelperDescriptor
{
TagName = "div",
TypeName = "foo1",
AssemblyName = "SomeAssembly",
};
var spanDescriptor = new TagHelperDescriptor
{
TagName = "span",
TypeName = "foo2",
AssemblyName = "SomeAssembly",
};
var descriptors = new TagHelperDescriptor[] { divDescriptor, spanDescriptor };
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptors = provider.GetDescriptors(
tagName: "foo",
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
Assert.Empty(retrievedDescriptors);
}
[Fact]
public void GetDescriptors_ReturnsCatchAllsWithEveryTagName()
{
// Arrange
var divDescriptor = new TagHelperDescriptor
{
TagName = "div",
TypeName = "foo1",
AssemblyName = "SomeAssembly",
};
var spanDescriptor = new TagHelperDescriptor
{
TagName = "span",
TypeName = "foo2",
AssemblyName = "SomeAssembly",
};
var catchAllDescriptor = new TagHelperDescriptor
{
TagName = TagHelperDescriptorProvider.ElementCatchAllTarget,
TypeName = "foo3",
AssemblyName = "SomeAssembly",
};
var descriptors = new TagHelperDescriptor[] { divDescriptor, spanDescriptor, catchAllDescriptor };
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var divDescriptors = provider.GetDescriptors(
tagName: "div",
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
var spanDescriptors = provider.GetDescriptors(
tagName: "span",
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
// For divs
Assert.Equal(2, divDescriptors.Count());
Assert.Contains(divDescriptor, divDescriptors);
Assert.Contains(catchAllDescriptor, divDescriptors);
// For spans
Assert.Equal(2, spanDescriptors.Count());
Assert.Contains(spanDescriptor, spanDescriptors);
Assert.Contains(catchAllDescriptor, spanDescriptors);
}
[Fact]
public void GetDescriptors_DuplicateDescriptorsAreNotPartOfTagHelperDescriptorPool()
{
// Arrange
var divDescriptor = new TagHelperDescriptor
{
TagName = "div",
TypeName = "foo1",
AssemblyName = "SomeAssembly",
};
var descriptors = new TagHelperDescriptor[] { divDescriptor, divDescriptor };
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var retrievedDescriptors = provider.GetDescriptors(
tagName: "div",
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
parentTagName: "p");
// Assert
var descriptor = Assert.Single(retrievedDescriptors);
Assert.Same(divDescriptor, descriptor);
}
private static TagHelperDescriptor CreatePrefixedDescriptor(string prefix, string tagName, string typeName)
{
return new TagHelperDescriptor
{
Prefix = prefix,
TagName = tagName,
TypeName = typeName,
AssemblyName = "SomeAssembly"
};
}
}
}

View File

@ -0,0 +1,556 @@
// 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;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
public class TagHelperDescriptorTest
{
[Fact]
public void Constructor_CorrectlyCreatesCopy()
{
// Arrange
var descriptor = new TagHelperDescriptor
{
Prefix = "prefix",
TagName = "tag-name",
TypeName = "TypeName",
AssemblyName = "AsssemblyName",
Attributes = new List<TagHelperAttributeDescriptor>
{
new TagHelperAttributeDescriptor
{
Name = "test-attribute",
PropertyName = "TestAttribute",
TypeName = "string"
}
},
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>
{
new TagHelperRequiredAttributeDescriptor
{
Name = "test-required-attribute"
}
},
AllowedChildren = new[] { "child" },
RequiredParent = "required parent",
TagStructure = TagStructure.NormalOrSelfClosing,
DesignTimeDescriptor = new TagHelperDesignTimeDescriptor()
};
descriptor.PropertyBag.Add("foo", "bar");
// Act
var copyDescriptor = new TagHelperDescriptor(descriptor);
// Assert
Assert.Equal(descriptor, copyDescriptor, CaseSensitiveTagHelperDescriptorComparer.Default);
Assert.Same(descriptor.Attributes, copyDescriptor.Attributes);
Assert.Same(descriptor.RequiredAttributes, copyDescriptor.RequiredAttributes);
}
[Fact]
public void TagHelperDescriptor_CanBeSerialized()
{
// Arrange
var descriptor = new TagHelperDescriptor
{
Prefix = "prefix:",
TagName = "tag name",
TypeName = "type name",
AssemblyName = "assembly name",
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "required attribute one",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch
},
new TagHelperRequiredAttributeDescriptor
{
Name = "required attribute two",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "something",
ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
}
},
AllowedChildren = new[] { "allowed child one" },
RequiredParent = "parent name",
DesignTimeDescriptor = new TagHelperDesignTimeDescriptor
{
Summary = "usage summary",
Remarks = "usage remarks",
OutputElementHint = "some-tag"
},
};
var expectedSerializedDescriptor =
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" +
$"[{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute one\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":1," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":null," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":0}}," +
$"{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute two\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":0," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":\"something\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":2}}]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\"]," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":{{" +
$"\"{ nameof(TagHelperDesignTimeDescriptor.Summary) }\":\"usage summary\"," +
$"\"{ nameof(TagHelperDesignTimeDescriptor.Remarks) }\":\"usage remarks\"," +
$"\"{ nameof(TagHelperDesignTimeDescriptor.OutputElementHint) }\":\"some-tag\"}}," +
$"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":{{}}}}";
// Act
var serializedDescriptor = JsonConvert.SerializeObject(descriptor);
// Assert
Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal);
}
[Fact]
public void TagHelperDescriptor_WithAttributes_CanBeSerialized()
{
// Arrange
var descriptor = new TagHelperDescriptor
{
Prefix = "prefix:",
TagName = "tag name",
TypeName = "type name",
AssemblyName = "assembly name",
Attributes = new[]
{
new TagHelperAttributeDescriptor
{
Name = "attribute one",
PropertyName = "property name",
TypeName = "property type name",
IsEnum = true,
},
new TagHelperAttributeDescriptor
{
Name = "attribute two",
PropertyName = "property name",
TypeName = typeof(string).FullName,
IsStringProperty = true
},
},
TagStructure = TagStructure.NormalOrSelfClosing
};
var expectedSerializedDescriptor =
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":null," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":1," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null," +
$"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":{{}}}}";
// Act
var serializedDescriptor = JsonConvert.SerializeObject(descriptor);
// Assert
Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal);
}
[Fact]
public void TagHelperDescriptor_WithIndexerAttributes_CanBeSerialized()
{
// Arrange
var descriptor = new TagHelperDescriptor
{
Prefix = "prefix:",
TagName = "tag name",
TypeName = "type name",
AssemblyName = "assembly name",
Attributes = new[]
{
new TagHelperAttributeDescriptor
{
Name = "attribute one",
PropertyName = "property name",
TypeName = "property type name",
IsIndexer = true,
IsEnum = true,
},
new TagHelperAttributeDescriptor
{
Name = "attribute two",
PropertyName = "property name",
TypeName = typeof(string).FullName,
IsIndexer = true,
IsEnum = false,
IsStringProperty = true
},
},
AllowedChildren = new[] { "allowed child one", "allowed child two" },
RequiredParent = "parent name"
};
var expectedSerializedDescriptor =
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null," +
$"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":{{}}}}";
// Act
var serializedDescriptor = JsonConvert.SerializeObject(descriptor);
// Assert
Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal);
}
[Fact]
public void TagHelperDescriptor_WithPropertyBagElements_CanBeSerialized()
{
// Arrange
var descriptor = new TagHelperDescriptor
{
Prefix = "prefix:",
TagName = "tag name",
TypeName = "type name",
AssemblyName = "assembly name"
};
descriptor.PropertyBag.Add("key one", "value one");
descriptor.PropertyBag.Add("key two", "value two");
var expectedSerializedDescriptor =
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":null," +
$"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null," +
$"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":" +
"{\"key one\":\"value one\",\"key two\":\"value two\"}}";
// Act
var serializedDescriptor = JsonConvert.SerializeObject(descriptor);
// Assert
Assert.Equal(expectedSerializedDescriptor, serializedDescriptor);
}
[Fact]
public void TagHelperDescriptor_CanBeDeserialized()
{
// Arrange
var serializedDescriptor =
$"{{\"{nameof(TagHelperDescriptor.Prefix)}\":\"prefix:\"," +
$"\"{nameof(TagHelperDescriptor.TagName)}\":\"tag name\"," +
$"\"{nameof(TagHelperDescriptor.FullTagName)}\":\"prefix:tag name\"," +
$"\"{nameof(TagHelperDescriptor.TypeName)}\":\"type name\"," +
$"\"{nameof(TagHelperDescriptor.AssemblyName)}\":\"assembly name\"," +
$"\"{nameof(TagHelperDescriptor.Attributes)}\":[]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" +
$"[{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute one\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":1," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":null," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":0}}," +
$"{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute two\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":0," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":\"something\"," +
$"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":2}}]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," +
$"\"{nameof(TagHelperDescriptor.TagStructure)}\":2," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":{{" +
$"\"{ nameof(TagHelperDesignTimeDescriptor.Summary) }\":\"usage summary\"," +
$"\"{ nameof(TagHelperDesignTimeDescriptor.Remarks) }\":\"usage remarks\"," +
$"\"{ nameof(TagHelperDesignTimeDescriptor.OutputElementHint) }\":\"some-tag\"}}}}";
var expectedDescriptor = new TagHelperDescriptor
{
Prefix = "prefix:",
TagName = "tag name",
TypeName = "type name",
AssemblyName = "assembly name",
RequiredAttributes = new[]
{
new TagHelperRequiredAttributeDescriptor
{
Name = "required attribute one",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch
},
new TagHelperRequiredAttributeDescriptor
{
Name = "required attribute two",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "something",
ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
}
},
AllowedChildren = new[] { "allowed child one", "allowed child two" },
RequiredParent = "parent name",
DesignTimeDescriptor = new TagHelperDesignTimeDescriptor
{
Summary = "usage summary",
Remarks = "usage remarks",
OutputElementHint = "some-tag"
}
};
// Act
var descriptor = JsonConvert.DeserializeObject<TagHelperDescriptor>(serializedDescriptor);
// Assert
Assert.NotNull(descriptor);
Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
Assert.Empty(descriptor.Attributes);
Assert.Equal(expectedDescriptor.RequiredAttributes, descriptor.RequiredAttributes, TagHelperRequiredAttributeDescriptorComparer.Default);
Assert.Equal(
expectedDescriptor.DesignTimeDescriptor,
descriptor.DesignTimeDescriptor,
TagHelperDesignTimeDescriptorComparer.Default);
Assert.Empty(descriptor.PropertyBag);
}
[Fact]
public void TagHelperDescriptor_WithAttributes_CanBeDeserialized()
{
// Arrange
var serializedDescriptor =
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":null," +
$"\"{nameof(TagHelperDescriptor.TagStructure)}\":0," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}";
var expectedDescriptor = new TagHelperDescriptor
{
Prefix = "prefix:",
TagName = "tag name",
TypeName = "type name",
AssemblyName = "assembly name",
Attributes = new[]
{
new TagHelperAttributeDescriptor
{
Name = "attribute one",
PropertyName = "property name",
TypeName = "property type name",
IsEnum = true,
},
new TagHelperAttributeDescriptor
{
Name = "attribute two",
PropertyName = "property name",
TypeName = typeof(string).FullName,
IsEnum = false,
IsStringProperty = true
},
},
AllowedChildren = new[] { "allowed child one", "allowed child two" }
};
// Act
var descriptor = JsonConvert.DeserializeObject<TagHelperDescriptor>(serializedDescriptor);
// Assert
Assert.NotNull(descriptor);
Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.Attributes, descriptor.Attributes, TagHelperAttributeDescriptorComparer.Default);
Assert.Empty(descriptor.RequiredAttributes);
Assert.Empty(descriptor.PropertyBag);
}
[Fact]
public void TagHelperDescriptor_WithIndexerAttributes_CanBeDeserialized()
{
// Arrange
var serializedDescriptor =
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," +
$"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":null," +
$"\"{nameof(TagHelperDescriptor.TagStructure)}\":1," +
$"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null," +
$"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":{{}}}}";
var expectedDescriptor = new TagHelperDescriptor
{
Prefix = "prefix:",
TagName = "tag name",
TypeName = "type name",
AssemblyName = "assembly name",
Attributes = new[]
{
new TagHelperAttributeDescriptor
{
Name = "attribute one",
PropertyName = "property name",
TypeName = "property type name",
IsIndexer = true,
IsEnum = true,
},
new TagHelperAttributeDescriptor
{
Name = "attribute two",
PropertyName = "property name",
TypeName = typeof(string).FullName,
IsIndexer = true,
IsEnum = false,
IsStringProperty = true
}
},
TagStructure = TagStructure.NormalOrSelfClosing
};
// Act
var descriptor = JsonConvert.DeserializeObject<TagHelperDescriptor>(serializedDescriptor);
// Assert
Assert.NotNull(descriptor);
Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.Attributes, descriptor.Attributes, TagHelperAttributeDescriptorComparer.Default);
Assert.Empty(descriptor.RequiredAttributes);
Assert.Empty(descriptor.PropertyBag);
}
[Fact]
public void TagHelperDescriptor_WithPropertyBagElements_CanBeDeserialized()
{
// Arrange
var serializedDescriptor =
$"{{\"{nameof(TagHelperDescriptor.Prefix)}\":\"prefix:\"," +
$"\"{nameof(TagHelperDescriptor.TagName)}\":\"tag name\"," +
$"\"{nameof(TagHelperDescriptor.TypeName)}\":\"type name\"," +
$"\"{nameof(TagHelperDescriptor.AssemblyName)}\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":" +
"{\"key one\":\"value one\",\"key two\":\"value two\"}}";
var expectedDescriptor = new TagHelperDescriptor
{
Prefix = "prefix:",
TagName = "tag name",
TypeName = "type name",
AssemblyName = "assembly name"
};
expectedDescriptor.PropertyBag.Add("key one", "value one");
expectedDescriptor.PropertyBag.Add("key two", "value two");
// Act
var descriptor = JsonConvert.DeserializeObject<TagHelperDescriptor>(serializedDescriptor);
// Assert
Assert.NotNull(descriptor);
Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
Assert.Empty(descriptor.Attributes);
Assert.Empty(descriptor.RequiredAttributes);
Assert.Equal(expectedDescriptor.PropertyBag["key one"], descriptor.PropertyBag["key one"]);
Assert.Equal(expectedDescriptor.PropertyBag["key two"], descriptor.PropertyBag["key two"]);
}
}
}

View File

@ -0,0 +1,47 @@
// 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;
using Microsoft.Extensions.Internal;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal class TagHelperDesignTimeDescriptorComparer : IEqualityComparer<TagHelperDesignTimeDescriptor>
{
public static readonly TagHelperDesignTimeDescriptorComparer Default =
new TagHelperDesignTimeDescriptorComparer();
private TagHelperDesignTimeDescriptorComparer()
{
}
public bool Equals(TagHelperDesignTimeDescriptor descriptorX, TagHelperDesignTimeDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}
Assert.NotNull(descriptorX);
Assert.NotNull(descriptorY);
Assert.Equal(descriptorX.Summary, descriptorY.Summary, StringComparer.Ordinal);
Assert.Equal(descriptorX.Remarks, descriptorY.Remarks, StringComparer.Ordinal);
Assert.Equal(descriptorX.OutputElementHint, descriptorY.OutputElementHint, StringComparer.Ordinal);
return true;
}
public int GetHashCode(TagHelperDesignTimeDescriptor descriptor)
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(descriptor.Summary, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.Remarks, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.OutputElementHint, StringComparer.Ordinal);
return hashCodeCombiner;
}
}
}

View File

@ -0,0 +1,423 @@
// 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;
using System.Linq;
using Microsoft.Extensions.Internal;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
public class TagHelperDirectiveSpanVisitorTest
{
public static TheoryData QuotedTagHelperDirectivesData
{
get
{
var factory = new SpanFactory();
// document, expectedDescriptors
return new TheoryData<MarkupBlock, IEnumerable<TagHelperDirectiveDescriptor>>
{
{
new MarkupBlock(factory.Code("\"*, someAssembly\"").AsAddTagHelper("*, someAssembly")),
new[]
{
new TagHelperDirectiveDescriptor
{
DirectiveText = "*, someAssembly",
DirectiveType = TagHelperDirectiveType.AddTagHelper
},
}
},
{
new MarkupBlock(factory.Code("\"*, someAssembly\"").AsRemoveTagHelper("*, someAssembly")),
new[]
{
new TagHelperDirectiveDescriptor
{
DirectiveText = "*, someAssembly",
DirectiveType = TagHelperDirectiveType.RemoveTagHelper
},
}
},
{
new MarkupBlock(factory.Code("\"th:\"").AsTagHelperPrefixDirective("th:")),
new[]
{
new TagHelperDirectiveDescriptor
{
DirectiveText = "th:",
DirectiveType = TagHelperDirectiveType.TagHelperPrefix
},
}
},
{
new MarkupBlock(factory.Code(" \"*, someAssembly \" ").AsAddTagHelper("*, someAssembly ")),
new[]
{
new TagHelperDirectiveDescriptor
{
DirectiveText = "*, someAssembly",
DirectiveType = TagHelperDirectiveType.AddTagHelper
},
}
},
{
new MarkupBlock(factory.Code(" \"*, someAssembly \" ").AsRemoveTagHelper("*, someAssembly ")),
new[]
{
new TagHelperDirectiveDescriptor
{
DirectiveText = "*, someAssembly",
DirectiveType = TagHelperDirectiveType.RemoveTagHelper
},
}
},
{
new MarkupBlock(factory.Code(" \" th :\"").AsTagHelperPrefixDirective(" th :")),
new[]
{
new TagHelperDirectiveDescriptor
{
DirectiveText = "th :",
DirectiveType = TagHelperDirectiveType.TagHelperPrefix
},
}
},
};
}
}
[Theory]
[MemberData(nameof(QuotedTagHelperDirectivesData))]
public void GetDescriptors_LocatesQuotedTagHelperDirectives_CreatesDirectiveDescriptors(
object document,
object expectedDescriptors)
{
// Arrange
var resolver = new TestTagHelperDescriptorResolver();
var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor(resolver, new ErrorSink());
// Act
tagHelperDirectiveSpanVisitor.GetDescriptors((MarkupBlock)document);
// Assert
Assert.Equal(
(IEnumerable<TagHelperDirectiveDescriptor>)expectedDescriptors,
resolver.DirectiveDescriptors,
TagHelperDirectiveDescriptorComparer.Default);
}
[Fact]
public void GetDescriptors_InvokesResolveOnceForAllDirectives()
{
// Arrange
var factory = new SpanFactory();
var resolver = new Mock<ITagHelperDescriptorResolver>();
resolver.Setup(mock => mock.Resolve(It.IsAny<TagHelperDescriptorResolutionContext>()))
.Returns(Enumerable.Empty<TagHelperDescriptor>());
var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor(
resolver.Object,
new ErrorSink());
var document = new MarkupBlock(
factory.Code("one").AsAddTagHelper("one"),
factory.Code("two").AsRemoveTagHelper("two"),
factory.Code("three").AsRemoveTagHelper("three"),
factory.Code("four").AsTagHelperPrefixDirective("four"));
// Act
tagHelperDirectiveSpanVisitor.GetDescriptors(document);
// Assert
resolver.Verify(mock => mock.Resolve(It.IsAny<TagHelperDescriptorResolutionContext>()), Times.Once);
}
[Fact]
public void GetDescriptors_LocatesTagHelperChunkGenerator_CreatesDirectiveDescriptors()
{
// Arrange
var factory = new SpanFactory();
var resolver = new TestTagHelperDescriptorResolver();
var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor(resolver, new ErrorSink());
var document = new MarkupBlock(
factory.Code("one").AsAddTagHelper("one"),
factory.Code("two").AsRemoveTagHelper("two"),
factory.Code("three").AsRemoveTagHelper("three"),
factory.Code("four").AsTagHelperPrefixDirective("four"));
var expectedDescriptors = new TagHelperDirectiveDescriptor[]
{
new TagHelperDirectiveDescriptor
{
DirectiveText = "one",
DirectiveType = TagHelperDirectiveType.AddTagHelper
},
new TagHelperDirectiveDescriptor
{
DirectiveText = "two",
DirectiveType = TagHelperDirectiveType.RemoveTagHelper
},
new TagHelperDirectiveDescriptor
{
DirectiveText = "three",
DirectiveType = TagHelperDirectiveType.RemoveTagHelper
},
new TagHelperDirectiveDescriptor
{
DirectiveText = "four",
DirectiveType = TagHelperDirectiveType.TagHelperPrefix
}
};
// Act
tagHelperDirectiveSpanVisitor.GetDescriptors(document);
// Assert
Assert.Equal(
expectedDescriptors,
resolver.DirectiveDescriptors,
TagHelperDirectiveDescriptorComparer.Default);
}
[Fact]
public void GetDescriptors_CanOverrideResolutionContext()
{
// Arrange
var factory = new SpanFactory();
var resolver = new TestTagHelperDescriptorResolver();
var expectedInitialDirectiveDescriptors = new TagHelperDirectiveDescriptor[]
{
new TagHelperDirectiveDescriptor
{
DirectiveText = "one",
DirectiveType = TagHelperDirectiveType.AddTagHelper
},
new TagHelperDirectiveDescriptor
{
DirectiveText = "two",
DirectiveType = TagHelperDirectiveType.RemoveTagHelper
},
new TagHelperDirectiveDescriptor
{
DirectiveText = "three",
DirectiveType = TagHelperDirectiveType.RemoveTagHelper
},
new TagHelperDirectiveDescriptor
{
DirectiveText = "four",
DirectiveType = TagHelperDirectiveType.TagHelperPrefix
}
};
var expectedEndDirectiveDescriptors = new TagHelperDirectiveDescriptor[]
{
new TagHelperDirectiveDescriptor
{
DirectiveText = "custom",
DirectiveType = TagHelperDirectiveType.AddTagHelper
}
};
var tagHelperDirectiveSpanVisitor = new CustomTagHelperDirectiveSpanVisitor(
resolver,
(descriptors, errorSink) =>
{
Assert.Equal(
expectedInitialDirectiveDescriptors,
descriptors,
TagHelperDirectiveDescriptorComparer.Default);
return new TagHelperDescriptorResolutionContext(expectedEndDirectiveDescriptors, errorSink);
});
var document = new MarkupBlock(
factory.Code("one").AsAddTagHelper("one"),
factory.Code("two").AsRemoveTagHelper("two"),
factory.Code("three").AsRemoveTagHelper("three"),
factory.Code("four").AsTagHelperPrefixDirective("four"));
// Act
tagHelperDirectiveSpanVisitor.GetDescriptors(document);
// Assert
Assert.Equal(expectedEndDirectiveDescriptors,
resolver.DirectiveDescriptors,
TagHelperDirectiveDescriptorComparer.Default);
}
[Fact]
public void GetDescriptors_LocatesTagHelperPrefixDirectiveChunkGenerator()
{
// Arrange
var factory = new SpanFactory();
var resolver = new TestTagHelperDescriptorResolver();
var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor(resolver, new ErrorSink());
var document = new MarkupBlock(
new DirectiveBlock(
factory.CodeTransition(),
factory
.MetaCode(SyntaxConstants.CSharp.TagHelperPrefixKeyword + " ")
.Accepts(AcceptedCharacters.None),
factory.Code("something").AsTagHelperPrefixDirective("something")));
var expectedDirectiveDescriptor =
new TagHelperDirectiveDescriptor
{
DirectiveText = "something",
DirectiveType = TagHelperDirectiveType.TagHelperPrefix
};
// Act
tagHelperDirectiveSpanVisitor.GetDescriptors(document);
// Assert
var directiveDescriptor = Assert.Single(resolver.DirectiveDescriptors);
Assert.Equal(
expectedDirectiveDescriptor,
directiveDescriptor,
TagHelperDirectiveDescriptorComparer.Default);
}
[Fact]
public void GetDescriptors_LocatesAddTagHelperChunkGenerator()
{
// Arrange
var factory = new SpanFactory();
var resolver = new TestTagHelperDescriptorResolver();
var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor(resolver, new ErrorSink());
var document = new MarkupBlock(
new DirectiveBlock(
factory.CodeTransition(),
factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ")
.Accepts(AcceptedCharacters.None),
factory.Code("something").AsAddTagHelper("something"))
);
var expectedRegistration = new TagHelperDirectiveDescriptor
{
DirectiveText = "something",
DirectiveType = TagHelperDirectiveType.AddTagHelper
};
// Act
tagHelperDirectiveSpanVisitor.GetDescriptors(document);
// Assert
var directiveDescriptor = Assert.Single(resolver.DirectiveDescriptors);
Assert.Equal(expectedRegistration, directiveDescriptor, TagHelperDirectiveDescriptorComparer.Default);
}
[Fact]
public void GetDescriptors_LocatesNestedRemoveTagHelperChunkGenerator()
{
// Arrange
var factory = new SpanFactory();
var resolver = new TestTagHelperDescriptorResolver();
var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor(resolver, new ErrorSink());
var document = new MarkupBlock(
new DirectiveBlock(
factory.CodeTransition(),
factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ")
.Accepts(AcceptedCharacters.None),
factory.Code("something").AsRemoveTagHelper("something"))
);
var expectedRegistration = new TagHelperDirectiveDescriptor
{
DirectiveText = "something",
DirectiveType = TagHelperDirectiveType.RemoveTagHelper
};
// Act
tagHelperDirectiveSpanVisitor.GetDescriptors(document);
// Assert
var directiveDescriptor = Assert.Single(resolver.DirectiveDescriptors);
Assert.Equal(expectedRegistration, directiveDescriptor, TagHelperDirectiveDescriptorComparer.Default);
}
[Fact]
public void GetDescriptors_RemoveTagHelperNotInDocument_DoesNotThrow()
{
// Arrange
var factory = new SpanFactory();
var tagHelperDirectiveSpanVisitor =
new TagHelperDirectiveSpanVisitor(
new TestTagHelperDescriptorResolver(),
new ErrorSink());
var document = new MarkupBlock(factory.Markup("Hello World"));
// Act
var descriptors = tagHelperDirectiveSpanVisitor.GetDescriptors(document);
Assert.Empty(descriptors);
}
private class TestTagHelperDescriptorResolver : ITagHelperDescriptorResolver
{
public TestTagHelperDescriptorResolver()
{
DirectiveDescriptors = new List<TagHelperDirectiveDescriptor>();
}
public List<TagHelperDirectiveDescriptor> DirectiveDescriptors { get; }
public IEnumerable<TagHelperDescriptor> Resolve(TagHelperDescriptorResolutionContext resolutionContext)
{
DirectiveDescriptors.AddRange(resolutionContext.DirectiveDescriptors);
return Enumerable.Empty<TagHelperDescriptor>();
}
}
private class TagHelperDirectiveDescriptorComparer : IEqualityComparer<TagHelperDirectiveDescriptor>
{
public static readonly TagHelperDirectiveDescriptorComparer Default =
new TagHelperDirectiveDescriptorComparer();
private TagHelperDirectiveDescriptorComparer()
{
}
public bool Equals(TagHelperDirectiveDescriptor directiveDescriptorX,
TagHelperDirectiveDescriptor directiveDescriptorY)
{
return string.Equals(directiveDescriptorX.DirectiveText,
directiveDescriptorY.DirectiveText,
StringComparison.Ordinal) &&
directiveDescriptorX.DirectiveType == directiveDescriptorY.DirectiveType;
}
public int GetHashCode(TagHelperDirectiveDescriptor directiveDescriptor)
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(base.GetHashCode());
hashCodeCombiner.Add(directiveDescriptor.DirectiveText);
hashCodeCombiner.Add(directiveDescriptor.DirectiveType);
return hashCodeCombiner;
}
}
private class CustomTagHelperDirectiveSpanVisitor : TagHelperDirectiveSpanVisitor
{
private Func<IEnumerable<TagHelperDirectiveDescriptor>,
ErrorSink,
TagHelperDescriptorResolutionContext> _replacer;
public CustomTagHelperDirectiveSpanVisitor(
ITagHelperDescriptorResolver descriptorResolver,
Func<IEnumerable<TagHelperDirectiveDescriptor>,
ErrorSink,
TagHelperDescriptorResolutionContext> replacer)
: base(descriptorResolver, new ErrorSink())
{
_replacer = replacer;
}
protected override TagHelperDescriptorResolutionContext GetTagHelperDescriptorResolutionContext(
IEnumerable<TagHelperDirectiveDescriptor> descriptors,
ErrorSink errorSink)
{
return _replacer(descriptors, errorSink);
}
}
}
}

View File

@ -0,0 +1,173 @@
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
public class TagHelperRequiredAttributeDescriptorTest
{
public static TheoryData RequiredAttributeDescriptorData
{
get
{
// requiredAttributeDescriptor, attributeName, attributeValue, expectedResult
return new TheoryData<TagHelperRequiredAttributeDescriptor, string, string, bool>
{
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key"
},
"KeY",
"value",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key"
},
"keys",
"value",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "route-",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
},
"ROUTE-area",
"manage",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "route-",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
},
"routearea",
"manage",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "route-",
NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch,
},
"route-",
"manage",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
},
"KeY",
"value",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
},
"keys",
"value",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "value",
ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch,
},
"key",
"value",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "key",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "value",
ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch,
},
"key",
"Value",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "class",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "btn",
ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
},
"class",
"btn btn-success",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "class",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "btn",
ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch,
},
"class",
"BTN btn-success",
false
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "href",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "#navigate",
ValueComparison = TagHelperRequiredAttributeValueComparison.SuffixMatch,
},
"href",
"/home/index#navigate",
true
},
{
new TagHelperRequiredAttributeDescriptor
{
Name = "href",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch,
Value = "#navigate",
ValueComparison = TagHelperRequiredAttributeValueComparison.SuffixMatch,
},
"href",
"/home/index#NAVigate",
false
},
};
}
}
[Theory]
[MemberData(nameof(RequiredAttributeDescriptorData))]
public void Matches_ReturnsExpectedResult(
object requiredAttributeDescriptor,
string attributeName,
string attributeValue,
bool expectedResult)
{
// Act
var result = ((TagHelperRequiredAttributeDescriptor)requiredAttributeDescriptor).IsMatch(attributeName, attributeValue);
// Assert
Assert.Equal(expectedResult, result);
}
}
}

View File

@ -0,0 +1,76 @@
// 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;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Evolution.TagHelpers;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
public class TagHelperRewritingTestBase : CsHtmlMarkupParserTestBase
{
internal void RunParseTreeRewriterTest(
string documentContent,
MarkupBlock expectedOutput,
params string[] tagNames)
{
RunParseTreeRewriterTest(
documentContent,
expectedOutput,
errors: Enumerable.Empty<RazorError>(),
tagNames: tagNames);
}
internal void RunParseTreeRewriterTest(
string documentContent,
MarkupBlock expectedOutput,
IEnumerable<RazorError> errors,
params string[] tagNames)
{
var providerContext = BuildProviderContext(tagNames);
EvaluateData(providerContext, documentContent, expectedOutput, errors);
}
internal TagHelperDescriptorProvider BuildProviderContext(params string[] tagNames)
{
var descriptors = new List<TagHelperDescriptor>();
foreach (var tagName in tagNames)
{
descriptors.Add(
new TagHelperDescriptor
{
TagName = tagName,
TypeName = tagName + "taghelper",
AssemblyName = "SomeAssembly"
});
}
return new TagHelperDescriptorProvider(descriptors);
}
internal void EvaluateData(
TagHelperDescriptorProvider provider,
string documentContent,
MarkupBlock expectedOutput,
IEnumerable<RazorError> expectedErrors)
{
var syntaxTree = ParseDocument(documentContent);
var errorSink = new ErrorSink();
var parseTreeRewriter = new TagHelperParseTreeRewriter(provider);
var actualTree = parseTreeRewriter.Rewrite(syntaxTree.Root, errorSink);
var allErrors = syntaxTree.Diagnostics.Concat(errorSink.Errors);
var actualErrors = allErrors
.OrderBy(error => error.Location.AbsoluteIndex)
.ToList();
EvaluateRazorErrors(actualErrors, expectedErrors.ToList());
EvaluateParseTree(actualTree, expectedOutput);
}
}
}