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:
parent
51fb0c993b
commit
26a1cf3cff
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"/>.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
/// <my-tag-helper></my-tag-helper>
|
||||
/// <!-- OR -->
|
||||
/// <my-tag-helper />
|
||||
/// </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>
|
||||
/// <my-tag-helper>
|
||||
/// <!-- OR -->
|
||||
/// <my-tag-helper />
|
||||
/// </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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <my-tag-helper></my-tag-helper> or <my-tag-helper />.
|
||||
/// </summary>
|
||||
NormalOrSelfClosing,
|
||||
|
||||
/// <summary>
|
||||
/// Element can be written as <my-tag-helper> or <my-tag-helper />.
|
||||
/// </summary>
|
||||
/// <remarks>Elements with a <see cref="WithoutEndTag"/> structure will never have any content.</remarks>
|
||||
WithoutEndTag
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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><<unknown>></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 '<{1} {0}{{ key }}="value">'.</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 <{0}> 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 (</{0}>) 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 <{0}> tag is not allowed by parent <{1}> 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>
|
||||
|
|
|
|||
|
|
@ -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 '<{1} {0}{{ key }}="value">'.
|
||||
/// </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 '<{1} {0}{{ key }}="value">'.
|
||||
/// </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 <{0}> 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 <{0}> 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 (</{0}>) 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 (</{0}>) 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 <{0}> tag is not allowed by parent <{1}> tag helper. Only child tags with name(s) '{2}' are allowed.
|
||||
/// </summary>
|
||||
internal static string TagHelperParseTreeRewriter_InvalidNestedTag
|
||||
{
|
||||
get { return GetString("TagHelperParseTreeRewriter_InvalidNestedTag"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <{0}> tag is not allowed by parent <{1}> 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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue