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()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format(
|
return string.Format(
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <see cref="RazorError"/>s collected.
|
/// <see cref="RazorError"/>s collected.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<RazorError> Errors => _errors;
|
public IReadOnlyList<RazorError> Errors => _errors;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tracks the given <paramref name="error"/>.
|
/// 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">
|
<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>
|
<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>
|
||||||
|
<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">
|
<data name="SectionExample_CS" xml:space="preserve">
|
||||||
<value>@section Header { ... }</value>
|
<value>@section Header { ... }</value>
|
||||||
<comment>In CSHTML, the @section keyword is case-sensitive and lowercase (as with all C# keywords)</comment>
|
<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">
|
<data name="Symbol_Unknown" xml:space="preserve">
|
||||||
<value><<unknown>></value>
|
<value><<unknown>></value>
|
||||||
</data>
|
</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">
|
<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>
|
<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>
|
</data>
|
||||||
|
|
|
||||||
|
|
@ -1078,6 +1078,22 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
return GetString("Parser_Context_Not_Set");
|
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>
|
/// <summary>
|
||||||
/// @section Header { ... }
|
/// @section Header { ... }
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1126,6 +1142,166 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
return GetString("Symbol_Unknown");
|
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>
|
/// <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}
|
/// 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>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -54,5 +54,27 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
_factory.Markup(content).Accepts(acceptedCharacters)
|
_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
|
internal class SectionBlock : Block
|
||||||
{
|
{
|
||||||
private const BlockType ThisBlockType = BlockType.Section;
|
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)
|
private static void EvaluateSyntaxTreeNode(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected)
|
||||||
{
|
{
|
||||||
if (actual == null)
|
if (actual == null)
|
||||||
|
|
@ -327,6 +356,11 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (actual is TagHelperBlock)
|
||||||
|
{
|
||||||
|
EvaluateTagHelperBlock(collector, actual as TagHelperBlock, expected as TagHelperBlock);
|
||||||
|
}
|
||||||
|
|
||||||
AddPassedMessage(collector, expected);
|
AddPassedMessage(collector, expected);
|
||||||
using (collector.Indent())
|
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)
|
private static void AddPassedMessage(ErrorCollector collector, SyntaxTreeNode expected)
|
||||||
{
|
{
|
||||||
collector.AddMessage("{0} - PASSED", 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