diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/Block.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/Block.cs
index e5a27dc5fa..f3bee78695 100644
--- a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/Block.cs
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/Block.cs
@@ -82,6 +82,26 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
}
+ public Span FindFirstDescendentSpan()
+ {
+ SyntaxTreeNode current = this;
+ while (current != null && current.IsBlock)
+ {
+ current = ((Block)current).Children.FirstOrDefault();
+ }
+ return current as Span;
+ }
+
+ public Span FindLastDescendentSpan()
+ {
+ SyntaxTreeNode current = this;
+ while (current != null && current.IsBlock)
+ {
+ current = ((Block)current).Children.LastOrDefault();
+ }
+ return current as Span;
+ }
+
public override string ToString()
{
return string.Format(
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ErrorSink.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ErrorSink.cs
index a048078561..7a37ab3159 100644
--- a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ErrorSink.cs
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ErrorSink.cs
@@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
///
/// s collected.
///
- public IEnumerable Errors => _errors;
+ public IReadOnlyList Errors => _errors;
///
/// Tracks the given .
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/HtmlAttributeValueStyle.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/HtmlAttributeValueStyle.cs
new file mode 100644
index 0000000000..1391d3f703
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/HtmlAttributeValueStyle.cs
@@ -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,
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ITagHelperDescriptorResolver.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ITagHelperDescriptorResolver.cs
new file mode 100644
index 0000000000..abe5283b66
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ITagHelperDescriptorResolver.cs
@@ -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
+{
+ ///
+ /// Contract used to resolve s.
+ ///
+ internal interface ITagHelperDescriptorResolver
+ {
+ ///
+ /// Resolves s based on the given .
+ ///
+ ///
+ /// used to resolve descriptors for the Razor page.
+ ///
+ /// An of s based
+ /// on the given .
+ IEnumerable Resolve(TagHelperDescriptorResolutionContext resolutionContext);
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperAttributeDescriptor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperAttributeDescriptor.cs
new file mode 100644
index 0000000000..2b724482ae
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperAttributeDescriptor.cs
@@ -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
+{
+ ///
+ /// A metadata class describing a tag helper attribute.
+ ///
+ internal class TagHelperAttributeDescriptor
+ {
+ private string _typeName;
+ private string _name;
+ private string _propertyName;
+
+ ///
+ /// Instantiates a new instance of the class.
+ ///
+ 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;
+ }
+
+ ///
+ /// Gets an indication whether this is used for dictionary indexer
+ /// assignments.
+ ///
+ ///
+ /// If true this should be associated with all HTML
+ /// attributes that have names starting with . Otherwise this
+ /// is used for property assignment and is only associated with an
+ /// HTML attribute that has the exact .
+ ///
+ ///
+ /// HTML attribute names are matched case-insensitively, regardless of .
+ ///
+ public bool IsIndexer { get; set; }
+
+ ///
+ /// Gets or sets an indication whether this property is an .
+ ///
+ public bool IsEnum { get; set; }
+
+ ///
+ /// Gets or sets an indication whether this property is of type or, if
+ /// is true, whether the indexer's value is of type .
+ ///
+ ///
+ /// If true the is for . This causes the Razor parser
+ /// to allow empty values for HTML attributes matching this . If
+ /// false empty values for such matching attributes lead to errors.
+ ///
+ public bool IsStringProperty { get; set; }
+
+ ///
+ /// The HTML attribute name or, if is true, the prefix for matching attribute
+ /// names.
+ ///
+ public string Name
+ {
+ get
+ {
+ return _name;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _name = value;
+ }
+ }
+
+
+ ///
+ /// The name of the CLR property that corresponds to the HTML attribute.
+ ///
+ public string PropertyName
+ {
+ get
+ {
+ return _propertyName;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _propertyName = value;
+ }
+ }
+
+ ///
+ /// The full name of the named (see ) property's or, if
+ /// is true, the full name of the indexer's value .
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// The that contains design time information about
+ /// this attribute.
+ ///
+ public TagHelperAttributeDesignTimeDescriptor DesignTimeDescriptor { get; set; }
+
+ ///
+ /// Determines whether HTML attribute matches this
+ /// .
+ ///
+ /// Name of the HTML attribute to check.
+ ///
+ /// true if this matches .
+ /// false otherwise.
+ ///
+ public bool IsNameMatch(string name)
+ {
+ if (IsIndexer)
+ {
+ return name.StartsWith(Name, StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ return string.Equals(name, Name, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperAttributeDesignTimeDescriptor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperAttributeDesignTimeDescriptor.cs
new file mode 100644
index 0000000000..93f6a12f7d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperAttributeDesignTimeDescriptor.cs
@@ -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
+{
+ ///
+ /// A metadata class containing information about tag helper use.
+ ///
+ internal class TagHelperAttributeDesignTimeDescriptor
+ {
+ ///
+ /// A summary of how to use a tag helper.
+ ///
+ public string Summary { get; set; }
+
+ ///
+ /// Remarks about how to use a tag helper.
+ ///
+ public string Remarks { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperAttributeNode.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperAttributeNode.cs
new file mode 100644
index 0000000000..9eff197efd
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperAttributeNode.cs
@@ -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; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperBlock.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperBlock.cs
new file mode 100644
index 0000000000..e8b68b706a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperBlock.cs
@@ -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
+{
+ ///
+ /// A that reprents a special HTML element.
+ ///
+ internal class TagHelperBlock : Block, IEquatable
+ {
+ private readonly SourceLocation _start;
+
+ ///
+ /// Instantiates a new instance of a .
+ ///
+ /// A used to construct a valid
+ /// .
+ public TagHelperBlock(TagHelperBlockBuilder source)
+ : base(source.Type, source.Children, source.ChunkGenerator)
+ {
+ TagName = source.TagName;
+ Descriptors = source.Descriptors;
+ Attributes = new List(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;
+ }
+ }
+ }
+
+ ///
+ /// Gets the unrewritten source start tag.
+ ///
+ /// This is used by design time to properly format s.
+ public Block SourceStartTag { get; }
+
+ ///
+ /// Gets the unrewritten source end tag.
+ ///
+ /// This is used by design time to properly format s.
+ public Block SourceEndTag { get; }
+
+ ///
+ /// Gets the HTML syntax of the element in the Razor source.
+ ///
+ public TagMode TagMode { get; }
+
+ ///
+ /// s for the HTML element.
+ ///
+ public IEnumerable Descriptors { get; }
+
+ ///
+ /// The HTML attributes.
+ ///
+ public IList Attributes { get; }
+
+ ///
+ public override SourceLocation Start
+ {
+ get
+ {
+ return _start;
+ }
+ }
+
+ ///
+ /// The HTML tag name.
+ ///
+ 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 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;
+ }
+ }
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ /// Determines whether two s are equal by comparing the ,
+ /// , , and
+ /// .
+ ///
+ /// The to check equality against.
+ ///
+ /// true if the current is equivalent to the given
+ /// , false otherwise.
+ ///
+ public bool Equals(TagHelperBlock other)
+ {
+ return base.Equals(other) &&
+ string.Equals(TagName, other.TagName, StringComparison.OrdinalIgnoreCase) &&
+ Attributes.SequenceEqual(other.Attributes);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ var hashCodeCombiner = HashCodeCombiner.Start();
+ hashCodeCombiner.Add(base.GetHashCode());
+ hashCodeCombiner.Add(TagName, StringComparer.OrdinalIgnoreCase);
+ hashCodeCombiner.Add(Attributes);
+
+ return hashCodeCombiner;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperBlockBuilder.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperBlockBuilder.cs
new file mode 100644
index 0000000000..d171a0080a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperBlockBuilder.cs
@@ -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
+{
+ ///
+ /// A used to create s.
+ ///
+ internal class TagHelperBlockBuilder : BlockBuilder
+ {
+ ///
+ /// Instantiates a new instance based on the given
+ /// .
+ ///
+ /// The original to copy data from.
+ public TagHelperBlockBuilder(TagHelperBlock original)
+ : base(original)
+ {
+ TagName = original.TagName;
+ Descriptors = original.Descriptors;
+ Attributes = new List(original.Attributes);
+ }
+
+ ///
+ /// Instantiates a new instance of the class
+ /// with the provided values.
+ ///
+ /// An HTML tag name.
+ /// HTML syntax of the element in the Razor source.
+ /// Starting location of the .
+ /// Attributes of the .
+ /// The s associated with the current HTML
+ /// tag.
+ public TagHelperBlockBuilder(
+ string tagName,
+ TagMode tagMode,
+ SourceLocation start,
+ IList attributes,
+ IEnumerable descriptors)
+ {
+ TagName = tagName;
+ TagMode = tagMode;
+ Start = start;
+ Descriptors = descriptors;
+ Attributes = new List(attributes);
+ Type = BlockType.Tag;
+ ChunkGenerator = new TagHelperChunkGenerator(descriptors);
+ }
+
+ // Internal for testing
+ internal TagHelperBlockBuilder(
+ string tagName,
+ TagMode tagMode,
+ IList attributes,
+ IEnumerable 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);
+ }
+ }
+
+ ///
+ /// Gets or sets the unrewritten source start tag.
+ ///
+ /// This is used by design time to properly format s.
+ public Block SourceStartTag { get; set; }
+
+ ///
+ /// Gets or sets the unrewritten source end tag.
+ ///
+ /// This is used by design time to properly format s.
+ public Block SourceEndTag { get; set; }
+
+ ///
+ /// Gets the HTML syntax of the element in the Razor source.
+ ///
+ public TagMode TagMode { get; }
+
+ ///
+ /// s for the HTML element.
+ ///
+ public IEnumerable Descriptors { get; }
+
+ ///
+ /// The HTML attributes.
+ ///
+ public IList Attributes { get; }
+
+ ///
+ /// The HTML tag name.
+ ///
+ public string TagName { get; set; }
+
+ ///
+ /// Constructs a new .
+ ///
+ /// A .
+ public override Block Build()
+ {
+ return new TagHelperBlock(this);
+ }
+
+ ///
+ ///
+ /// Sets the to null and clears the .
+ ///
+ public override void Reset()
+ {
+ TagName = null;
+
+ if (Attributes != null)
+ {
+ Attributes.Clear();
+ }
+
+ base.Reset();
+ }
+
+ ///
+ /// The starting of the tag helper.
+ ///
+ public SourceLocation Start { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperBlockRewriter.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperBlockRewriter.cs
new file mode 100644
index 0000000000..baa2c90834
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperBlockRewriter.cs
@@ -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 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 GetTagAttributes(
+ string tagName,
+ bool validStructure,
+ Block tagBlock,
+ IEnumerable 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();
+
+ // We skip the first child "" 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 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 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:
+ var htmlSymbols = span.Symbols.OfType().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: 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:
+ //
+
+ 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.
+ if (builder.Children.Count == 1)
+ {
+ return TryParseSpan(childSpan, descriptors, errorSink);
+ }
+
+ var nameSymbols = childSpan
+ .Symbols
+ .OfType()
+ .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: where @value results in the test "hello world".
+ // This way, the above code would render .
+ 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.
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 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()
+ .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 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 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 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; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperChunkGenerator.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperChunkGenerator.cs
new file mode 100644
index 0000000000..ae1bb1aa9e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperChunkGenerator.cs
@@ -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 _tagHelperDescriptors;
+
+ ///
+ /// Instantiates a new .
+ ///
+ ///
+ /// s associated with the current HTML tag.
+ ///
+ public TagHelperChunkGenerator(IEnumerable 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();
+
+ //// 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptor.cs
new file mode 100644
index 0000000000..b81feb67a9
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptor.cs
@@ -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
+{
+ ///
+ /// A metadata class describing a tag helper.
+ ///
+ internal class TagHelperDescriptor
+ {
+ private string _prefix = string.Empty;
+ private string _tagName;
+ private string _typeName;
+ private string _assemblyName;
+ private IDictionary _propertyBag;
+ private IEnumerable _attributes =
+ Enumerable.Empty();
+ private IEnumerable _requiredAttributes =
+ Enumerable.Empty();
+
+ ///
+ /// Creates a new .
+ ///
+ public TagHelperDescriptor()
+ {
+ }
+
+ ///
+ /// Creates a shallow copy of the given .
+ ///
+ /// The to copy.
+ 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);
+ }
+ }
+
+ ///
+ /// Text used as a required prefix when matching HTML start and end tags in the Razor source to available
+ /// tag helpers.
+ ///
+ public string Prefix
+ {
+ get
+ {
+ return _prefix;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _prefix = value;
+ }
+ }
+
+ ///
+ /// The tag name that the tag helper should target.
+ ///
+ public string TagName
+ {
+ get
+ {
+ return _tagName;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _tagName = value;
+ }
+ }
+
+ ///
+ /// The full tag name that is required for the tag helper to target an HTML element.
+ ///
+ /// This is equivalent to and concatenated.
+ public string FullTagName
+ {
+ get
+ {
+ return Prefix + TagName;
+ }
+ }
+
+ ///
+ /// The full name of the tag helper class.
+ ///
+ public string TypeName
+ {
+ get
+ {
+ return _typeName;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _typeName = value;
+ }
+ }
+
+ ///
+ /// The name of the assembly containing the tag helper class.
+ ///
+ public string AssemblyName
+ {
+ get
+ {
+ return _assemblyName;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _assemblyName = value;
+ }
+ }
+
+ ///
+ /// The list of attributes the tag helper expects.
+ ///
+ public IEnumerable Attributes
+ {
+ get
+ {
+ return _attributes;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _attributes = value;
+ }
+ }
+
+ ///
+ /// The list of required attribute names the tag helper expects to target an element.
+ ///
+ ///
+ /// * at the end of an attribute name acts as a prefix match.
+ ///
+ public IEnumerable RequiredAttributes
+ {
+ get
+ {
+ return _requiredAttributes;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _requiredAttributes = value;
+ }
+ }
+
+ ///
+ /// Get the names of elements allowed as children.
+ ///
+ /// null indicates all children are allowed.
+ public IEnumerable AllowedChildren { get; set; }
+
+ ///
+ /// Get the name of the HTML element required as the immediate parent.
+ ///
+ /// null indicates no restriction on parent tag.
+ public string RequiredParent { get; set; }
+
+ ///
+ /// The expected tag structure.
+ ///
+ ///
+ /// If and no other tag helpers applying to the same element specify
+ /// their the behavior is used:
+ ///
+ ///
+ /// <my-tag-helper></my-tag-helper>
+ /// <!-- OR -->
+ /// <my-tag-helper />
+ ///
+ /// Otherwise, if another tag helper applying to the same element does specify their behavior, that behavior
+ /// is used.
+ ///
+ ///
+ /// If HTML elements can be written in the following formats:
+ ///
+ /// <my-tag-helper>
+ /// <!-- OR -->
+ /// <my-tag-helper />
+ ///
+ ///
+ ///
+ public TagStructure TagStructure { get; set; }
+
+ ///
+ /// The that contains design time information about this
+ /// tag helper.
+ ///
+ public TagHelperDesignTimeDescriptor DesignTimeDescriptor { get; set; }
+
+ ///
+ /// A dictionary containing additional information about the .
+ ///
+ public IDictionary PropertyBag
+ {
+ get
+ {
+ if (_propertyBag == null)
+ {
+ _propertyBag = new Dictionary();
+ }
+
+ return _propertyBag;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptorComparer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptorComparer.cs
new file mode 100644
index 0000000000..abac00462c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptorComparer.cs
@@ -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
+{
+ ///
+ /// An used to check equality between
+ /// two s.
+ ///
+ internal class TagHelperDescriptorComparer : IEqualityComparer
+ {
+ ///
+ /// A default instance of the .
+ ///
+ public static readonly TagHelperDescriptorComparer Default = new TagHelperDescriptorComparer();
+
+ ///
+ /// Initializes a new instance.
+ ///
+ protected TagHelperDescriptorComparer()
+ {
+ }
+
+ ///
+ ///
+ /// Determines equality based on ,
+ /// , ,
+ /// , ,
+ /// and .
+ /// Ignores because it can be inferred directly from
+ /// and .
+ ///
+ 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));
+ }
+
+ ///
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptorProvider.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptorProvider.cs
new file mode 100644
index 0000000000..3d7356f81c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptorProvider.cs
@@ -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
+{
+ ///
+ /// Enables retrieval of 's.
+ ///
+ internal class TagHelperDescriptorProvider
+ {
+ public const string ElementCatchAllTarget = "*";
+
+ private IDictionary> _registrations;
+ private string _tagHelperPrefix;
+
+ ///
+ /// Instantiates a new instance of the .
+ ///
+ /// The descriptors that the will pull from.
+ public TagHelperDescriptorProvider(IEnumerable descriptors)
+ {
+ _registrations = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ // Populate our registrations
+ foreach (var descriptor in descriptors)
+ {
+ Register(descriptor);
+ }
+ }
+
+ ///
+ /// Gets all tag helpers that match the given .
+ ///
+ /// The name of the HTML tag to match. Providing a '*' tag name
+ /// retrieves catch-all s (descriptors that target every tag).
+ /// Attributes the HTML element must contain to match.
+ /// The parent tag name of the given tag.
+ /// s that apply to the given .
+ /// Will return an empty if no s are
+ /// found.
+ public IEnumerable GetDescriptors(
+ string tagName,
+ IEnumerable> 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();
+ }
+
+ HashSet catchAllDescriptors;
+ IEnumerable descriptors;
+
+ // Ensure there's a HashSet to use.
+ if (!_registrations.TryGetValue(ElementCatchAllTarget, out catchAllDescriptors))
+ {
+ descriptors = new HashSet(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 matchingDescriptors;
+ if (_registrations.TryGetValue(tagName, out matchingDescriptors))
+ {
+ descriptors = matchingDescriptors.Concat(descriptors);
+ }
+
+ var applicableDescriptors = new List();
+ 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> attributes)
+ {
+ return descriptor.RequiredAttributes.All(
+ requiredAttribute => attributes.Any(
+ attribute => requiredAttribute.IsMatch(attribute.Key, attribute.Value)));
+ }
+
+ private void Register(TagHelperDescriptor descriptor)
+ {
+ HashSet 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(TagHelperDescriptorComparer.Default);
+ _registrations[registrationKey] = descriptorSet;
+ }
+
+ descriptorSet.Add(descriptor);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptorResolutionContext.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptorResolutionContext.cs
new file mode 100644
index 0000000000..ee0d6767eb
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDescriptorResolutionContext.cs
@@ -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
+{
+ ///
+ /// Contains information needed to resolve s.
+ ///
+ internal class TagHelperDescriptorResolutionContext
+ {
+ // Internal for testing purposes
+ internal TagHelperDescriptorResolutionContext(IEnumerable directiveDescriptors)
+ : this(directiveDescriptors, new ErrorSink())
+ {
+ }
+
+ ///
+ /// Instantiates a new instance of .
+ ///
+ /// s used to resolve
+ /// s.
+ /// Used to aggregate s.
+ public TagHelperDescriptorResolutionContext(
+ IEnumerable directiveDescriptors,
+ ErrorSink errorSink)
+ {
+ if (directiveDescriptors == null)
+ {
+ throw new ArgumentNullException(nameof(directiveDescriptors));
+ }
+
+ if (errorSink == null)
+ {
+ throw new ArgumentNullException(nameof(errorSink));
+ }
+
+ DirectiveDescriptors = new List(directiveDescriptors);
+ ErrorSink = errorSink;
+ }
+
+ ///
+ /// s used to resolve s.
+ ///
+ public IList DirectiveDescriptors { get; private set; }
+
+ ///
+ /// Used to aggregate s.
+ ///
+ public ErrorSink ErrorSink { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDesignTimeDescriptor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDesignTimeDescriptor.cs
new file mode 100644
index 0000000000..b5c120189f
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDesignTimeDescriptor.cs
@@ -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
+{
+ ///
+ /// A metadata class containing design time information about a tag helper.
+ ///
+ internal class TagHelperDesignTimeDescriptor
+ {
+ ///
+ /// A summary of how to use a tag helper.
+ ///
+ public string Summary { get; set; }
+
+ ///
+ /// Remarks about how to use a tag helper.
+ ///
+ public string Remarks { get; set; }
+
+ ///
+ /// The HTML element a tag helper may output.
+ ///
+ ///
+ /// In IDEs supporting IntelliSense, may override the HTML information provided at design time.
+ ///
+ public string OutputElementHint { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDirectiveDescriptor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDirectiveDescriptor.cs
new file mode 100644
index 0000000000..3b2652e548
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDirectiveDescriptor.cs
@@ -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
+{
+ ///
+ /// Contains information needed to resolve s.
+ ///
+ internal class TagHelperDirectiveDescriptor
+ {
+ private string _directiveText;
+
+ ///
+ /// A used to find tag helper s.
+ ///
+ public string DirectiveText
+ {
+ get
+ {
+ return _directiveText;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _directiveText = value;
+ }
+ }
+
+ ///
+ /// The of the directive.
+ ///
+ public SourceLocation Location { get; set; } = SourceLocation.Zero;
+
+ ///
+ /// The of this directive.
+ ///
+ public TagHelperDirectiveType DirectiveType { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDirectiveSpanVisitor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDirectiveSpanVisitor.cs
new file mode 100644
index 0000000000..fb6f7397ee
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDirectiveSpanVisitor.cs
@@ -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 _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 GetDescriptors(Block root)
+ {
+ if (root == null)
+ {
+ throw new ArgumentNullException(nameof(root));
+ }
+
+ _directiveDescriptors = new List();
+
+ // 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 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDirectiveType.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDirectiveType.cs
new file mode 100644
index 0000000000..931012bbee
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperDirectiveType.cs
@@ -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
+{
+ ///
+ /// The type of tag helper directive.
+ ///
+ internal enum TagHelperDirectiveType
+ {
+ ///
+ /// An @addTagHelper directive.
+ ///
+ AddTagHelper,
+
+ ///
+ /// A @removeTagHelper directive.
+ ///
+ RemoveTagHelper,
+
+ ///
+ /// A @tagHelperPrefix directive.
+ ///
+ TagHelperPrefix
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperParseTreeRewriter.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperParseTreeRewriter.cs
new file mode 100644
index 0000000000..1dec6d1b99
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperParseTreeRewriter.cs
@@ -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 VoidElements = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "area",
+ "base",
+ "br",
+ "col",
+ "command",
+ "embed",
+ "hr",
+ "img",
+ "input",
+ "keygen",
+ "link",
+ "meta",
+ "param",
+ "source",
+ "track",
+ "wbr"
+ };
+
+ private readonly List> _htmlAttributeTracker;
+ private readonly StringBuilder _attributeValueBuilder;
+ private readonly TagHelperDescriptorProvider _provider;
+ private readonly Stack _trackerStack;
+ private readonly Stack _blockStack;
+ private TagHelperBlockTracker _currentTagHelperTracker;
+ private BlockBuilder _currentBlock;
+ private string _currentParentTagName;
+
+ public TagHelperParseTreeRewriter(TagHelperDescriptorProvider provider)
+ {
+ _provider = provider;
+ _trackerStack = new Stack();
+ _blockStack = new Stack();
+ _attributeValueBuilder = new StringBuilder();
+ _htmlAttributeTracker = new List>();
+ }
+
+ 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 @
would cause an error because there's no
+ // matching end
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();
+
+ 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: 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: where we're at the inside .
+ if (tracker.OpenMatchingTags > 0)
+ {
+ tracker.OpenMatchingTags--;
+
+ return false;
+ }
+
+ ValidateTagSyntax(tagName, tagBlock, errorSink);
+
+ BuildCurrentlyTrackedTagHelperBlock(tagBlock);
+ }
+ else
+ {
+ descriptors = _provider.GetDescriptors(
+ tagName,
+ attributes: Enumerable.Empty>(),
+ 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> 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>();
+ }
+
+ _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(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 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 _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 AllowedChildren { get; }
+
+ public IEnumerable 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;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeDescriptor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeDescriptor.cs
new file mode 100644
index 0000000000..fc6c46298d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeDescriptor.cs
@@ -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
+{
+ ///
+ /// A metadata class describing a required tag helper attribute.
+ ///
+ internal class TagHelperRequiredAttributeDescriptor
+ {
+ ///
+ /// The HTML attribute name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// The comparison method to use for when determining if an HTML attribute name matches.
+ ///
+ public TagHelperRequiredAttributeNameComparison NameComparison { get; set; }
+
+ ///
+ /// The HTML attribute value.
+ ///
+ public string Value { get; set; }
+
+ ///
+ /// The comparison method to use for when determining if an HTML attribute value matches.
+ ///
+ public TagHelperRequiredAttributeValueComparison ValueComparison { get; set; }
+
+ ///
+ /// Determines if the current matches the given
+ /// and .
+ ///
+ /// An HTML attribute name.
+ /// An HTML attribute value.
+ /// true if the current matches
+ /// and ; false otherwise.
+ 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;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeDescriptorComparer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeDescriptorComparer.cs
new file mode 100644
index 0000000000..aeffbf27d2
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeDescriptorComparer.cs
@@ -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
+{
+ ///
+ /// An used to check equality between
+ /// two s.
+ ///
+ internal class TagHelperRequiredAttributeDescriptorComparer : IEqualityComparer
+ {
+ ///
+ /// A default instance of the .
+ ///
+ public static readonly TagHelperRequiredAttributeDescriptorComparer Default =
+ new TagHelperRequiredAttributeDescriptorComparer();
+
+ ///
+ /// Initializes a new instance.
+ ///
+ protected TagHelperRequiredAttributeDescriptorComparer()
+ {
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ 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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeNameComparison.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeNameComparison.cs
new file mode 100644
index 0000000000..22c7eb36df
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeNameComparison.cs
@@ -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
+{
+ ///
+ /// Acceptable comparison modes.
+ ///
+ internal enum TagHelperRequiredAttributeNameComparison
+ {
+ ///
+ /// HTML attribute name case insensitively matches .
+ ///
+ FullMatch,
+
+ ///
+ /// HTML attribute name case insensitively starts with .
+ ///
+ PrefixMatch,
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeValueComparison.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeValueComparison.cs
new file mode 100644
index 0000000000..80d25ce4a6
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagHelperRequiredAttributeValueComparison.cs
@@ -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
+{
+ ///
+ /// Acceptable comparison modes.
+ ///
+ internal enum TagHelperRequiredAttributeValueComparison
+ {
+ ///
+ /// HTML attribute value always matches .
+ ///
+ None,
+
+ ///
+ /// HTML attribute value case sensitively matches .
+ ///
+ FullMatch,
+
+ ///
+ /// HTML attribute value case sensitively starts with .
+ ///
+ PrefixMatch,
+
+ ///
+ /// HTML attribute value case sensitively ends with .
+ ///
+ SuffixMatch,
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagMode.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagMode.cs
new file mode 100644
index 0000000000..1fe181a7c4
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagMode.cs
@@ -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
+{
+ ///
+ /// The mode in which an element should render.
+ ///
+ internal enum TagMode
+ {
+ ///
+ /// Include both start and end tags.
+ ///
+ StartTagAndEndTag,
+
+ ///
+ /// A self-closed tag.
+ ///
+ SelfClosing,
+
+ ///
+ /// Only a start tag.
+ ///
+ StartTagOnly
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagStructure.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagStructure.cs
new file mode 100644
index 0000000000..3a5b297d88
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TagStructure.cs
@@ -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
+{
+ ///
+ /// The structure the element should be written in.
+ ///
+ internal enum TagStructure
+ {
+ ///
+ /// If no other tag helper applies to the same element and specifies a ,
+ /// will be used.
+ ///
+ Unspecified,
+
+ ///
+ /// Element can be written as <my-tag-helper></my-tag-helper> or <my-tag-helper />.
+ ///
+ NormalOrSelfClosing,
+
+ ///
+ /// Element can be written as <my-tag-helper> or <my-tag-helper />.
+ ///
+ /// Elements with a structure will never have any content.
+ WithoutEndTag
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TypeBasedTagHelperDescriptorComparer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TypeBasedTagHelperDescriptorComparer.cs
new file mode 100644
index 0000000000..82ab897d63
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/TypeBasedTagHelperDescriptorComparer.cs
@@ -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
+{
+ ///
+ /// An that checks equality between two
+ /// s using only their s and
+ /// s.
+ ///
+ ///
+ /// This class is intended for scenarios where Reflection-based information is all important i.e.
+ /// , , and related
+ /// properties are not relevant.
+ ///
+ internal class TypeBasedTagHelperDescriptorComparer : IEqualityComparer
+ {
+ ///
+ /// A default instance of the .
+ ///
+ public static readonly TypeBasedTagHelperDescriptorComparer Default =
+ new TypeBasedTagHelperDescriptorComparer();
+
+ private TypeBasedTagHelperDescriptorComparer()
+ {
+ }
+
+ ///
+ ///
+ /// Determines equality based on and
+ /// .
+ ///
+ 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);
+ }
+
+ ///
+ 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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/LegacyResources.resx b/src/Microsoft.AspNetCore.Razor.Evolution/LegacyResources.resx
index d567e51af2..e995b2f853 100644
--- a/src/Microsoft.AspNetCore.Razor.Evolution/LegacyResources.resx
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/LegacyResources.resx
@@ -335,6 +335,9 @@ Instead, wrap the contents of the block in "{{}}":
Parser was started with a null Context property. The Context property must be set BEFORE calling any methods on the parser.
+
+ Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound attributes of type '{2}' cannot be empty or contain only whitespace.
+
@section Header { ... }In CSHTML, the @section keyword is case-sensitive and lowercase (as with all C# keywords)
@@ -345,6 +348,36 @@ Instead, wrap the contents of the block in "{{}}":
<<unknown>>
+
+ The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '<{1} {0}{{ key }}="value">'.
+
+
+ TagHelper attributes must be well-formed.
+
+
+ The parent <{0}> tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed.
+
+
+ Found an end tag (</{0}>) for tag helper '{1}' with tag structure that disallows an end tag ('{2}').
+
+
+ Tag helpers '{0}' and '{1}' targeting element '{2}' must not expect different {3} values.
+
+
+ The <{0}> tag is not allowed by parent <{1}> tag helper. Only child tags with name(s) '{2}' are allowed.
+
+
+ Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing.
+
+
+ Missing close angle for tag helper '{0}'.
+
+
+ Tag Helper '{0}'s attributes must have names.
+
+
+ The tag helper '{0}' must not have C# in the element's attribute declaration area.
+
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}
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Properties/LegacyResources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Properties/LegacyResources.Designer.cs
index b4aa9a11b4..76c0a0b5e6 100644
--- a/src/Microsoft.AspNetCore.Razor.Evolution/Properties/LegacyResources.Designer.cs
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/Properties/LegacyResources.Designer.cs
@@ -1078,6 +1078,22 @@ namespace Microsoft.AspNetCore.Razor.Evolution
return GetString("Parser_Context_Not_Set");
}
+ ///
+ /// Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound attributes of type '{2}' cannot be empty or contain only whitespace.
+ ///
+ internal static string RewriterError_EmptyTagHelperBoundAttribute
+ {
+ get { return GetString("RewriterError_EmptyTagHelperBoundAttribute"); }
+ }
+
+ ///
+ /// Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound attributes of type '{2}' cannot be empty or contain only whitespace.
+ ///
+ internal static string FormatRewriterError_EmptyTagHelperBoundAttribute(object p0, object p1, object p2)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("RewriterError_EmptyTagHelperBoundAttribute"), p0, p1, p2);
+ }
+
///
/// @section Header { ... }
///
@@ -1126,6 +1142,166 @@ namespace Microsoft.AspNetCore.Razor.Evolution
return GetString("Symbol_Unknown");
}
+ ///
+ /// The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '<{1} {0}{{ key }}="value">'.
+ ///
+ internal static string TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey
+ {
+ get { return GetString("TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey"); }
+ }
+
+ ///
+ /// The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '<{1} {0}{{ key }}="value">'.
+ ///
+ internal static string FormatTagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey"), p0, p1);
+ }
+
+ ///
+ /// TagHelper attributes must be well-formed.
+ ///
+ internal static string TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed
+ {
+ get { return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed"); }
+ }
+
+ ///
+ /// TagHelper attributes must be well-formed.
+ ///
+ internal static string FormatTagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed()
+ {
+ return GetString("TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed");
+ }
+
+ ///
+ /// The parent <{0}> tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed.
+ ///
+ internal static string TagHelperParseTreeRewriter_CannotHaveNonTagContent
+ {
+ get { return GetString("TagHelperParseTreeRewriter_CannotHaveNonTagContent"); }
+ }
+
+ ///
+ /// The parent <{0}> tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed.
+ ///
+ internal static string FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_CannotHaveNonTagContent"), p0, p1);
+ }
+
+ ///
+ /// Found an end tag (</{0}>) for tag helper '{1}' with tag structure that disallows an end tag ('{2}').
+ ///
+ internal static string TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag
+ {
+ get { return GetString("TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag"); }
+ }
+
+ ///
+ /// Found an end tag (</{0}>) for tag helper '{1}' with tag structure that disallows an end tag ('{2}').
+ ///
+ internal static string FormatTagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag(object p0, object p1, object p2)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag"), p0, p1, p2);
+ }
+
+ ///
+ /// Tag helpers '{0}' and '{1}' targeting element '{2}' must not expect different {3} values.
+ ///
+ internal static string TagHelperParseTreeRewriter_InconsistentTagStructure
+ {
+ get { return GetString("TagHelperParseTreeRewriter_InconsistentTagStructure"); }
+ }
+
+ ///
+ /// Tag helpers '{0}' and '{1}' targeting element '{2}' must not expect different {3} values.
+ ///
+ 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);
+ }
+
+ ///
+ /// The <{0}> tag is not allowed by parent <{1}> tag helper. Only child tags with name(s) '{2}' are allowed.
+ ///
+ internal static string TagHelperParseTreeRewriter_InvalidNestedTag
+ {
+ get { return GetString("TagHelperParseTreeRewriter_InvalidNestedTag"); }
+ }
+
+ ///
+ /// The <{0}> tag is not allowed by parent <{1}> tag helper. Only child tags with name(s) '{2}' are allowed.
+ ///
+ internal static string FormatTagHelperParseTreeRewriter_InvalidNestedTag(object p0, object p1, object p2)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_InvalidNestedTag"), p0, p1, p2);
+ }
+
+ ///
+ /// Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing.
+ ///
+ internal static string TagHelpersParseTreeRewriter_FoundMalformedTagHelper
+ {
+ get { return GetString("TagHelpersParseTreeRewriter_FoundMalformedTagHelper"); }
+ }
+
+ ///
+ /// Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing.
+ ///
+ internal static string FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpersParseTreeRewriter_FoundMalformedTagHelper"), p0);
+ }
+
+ ///
+ /// Missing close angle for tag helper '{0}'.
+ ///
+ internal static string TagHelpersParseTreeRewriter_MissingCloseAngle
+ {
+ get { return GetString("TagHelpersParseTreeRewriter_MissingCloseAngle"); }
+ }
+
+ ///
+ /// Missing close angle for tag helper '{0}'.
+ ///
+ internal static string FormatTagHelpersParseTreeRewriter_MissingCloseAngle(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpersParseTreeRewriter_MissingCloseAngle"), p0);
+ }
+
+ ///
+ /// Tag Helper '{0}'s attributes must have names.
+ ///
+ internal static string TagHelpers_AttributesMustHaveAName
+ {
+ get { return GetString("TagHelpers_AttributesMustHaveAName"); }
+ }
+
+ ///
+ /// Tag Helper '{0}'s attributes must have names.
+ ///
+ internal static string FormatTagHelpers_AttributesMustHaveAName(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_AttributesMustHaveAName"), p0);
+ }
+
+ ///
+ /// The tag helper '{0}' must not have C# in the element's attribute declaration area.
+ ///
+ internal static string TagHelpers_CannotHaveCSharpInTagDeclaration
+ {
+ get { return GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"); }
+ }
+
+ ///
+ /// The tag helper '{0}' must not have C# in the element's attribute declaration area.
+ ///
+ internal static string FormatTagHelpers_CannotHaveCSharpInTagDeclaration(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"), p0);
+ }
+
///
/// 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}
///
diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockFactory.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockFactory.cs
index d289a60977..a414aa7b42 100644
--- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockFactory.cs
+++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockFactory.cs
@@ -54,5 +54,27 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
_factory.Markup(content).Accepts(acceptedCharacters)
);
}
+
+ public Block TagHelperBlock(
+ string tagName,
+ TagMode tagMode,
+ SourceLocation start,
+ Block startTag,
+ SyntaxTreeNode[] children,
+ Block endTag)
+ {
+ var builder = new TagHelperBlockBuilder(
+ tagName,
+ tagMode,
+ attributes: new List(),
+ children: children)
+ {
+ Start = start,
+ SourceStartTag = startTag,
+ SourceEndTag = endTag
+ };
+
+ return builder.Build();
+ }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockTypes.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockTypes.cs
index d05d4d3f4c..6f9c4789cc 100644
--- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockTypes.cs
+++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockTypes.cs
@@ -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())
+ {
+ }
+
+ public MarkupTagHelperBlock(string tagName, TagMode tagMode)
+ : this(tagName, tagMode, new List())
+ {
+ }
+
+ public MarkupTagHelperBlock(
+ string tagName,
+ IList attributes)
+ : this(tagName, TagMode.StartTagAndEndTag, attributes, children: new SyntaxTreeNode[0])
+ {
+ }
+
+ public MarkupTagHelperBlock(
+ string tagName,
+ TagMode tagMode,
+ IList attributes)
+ : this(tagName, tagMode, attributes, new SyntaxTreeNode[0])
+ {
+ }
+
+ public MarkupTagHelperBlock(string tagName, params SyntaxTreeNode[] children)
+ : this(
+ tagName,
+ TagMode.StartTagAndEndTag,
+ attributes: new List(),
+ children: children)
+ {
+ }
+
+ public MarkupTagHelperBlock(string tagName, TagMode tagMode, params SyntaxTreeNode[] children)
+ : this(tagName, tagMode, new List(), children)
+ {
+ }
+
+ public MarkupTagHelperBlock(
+ string tagName,
+ IList attributes,
+ params SyntaxTreeNode[] children)
+ : base(new TagHelperBlockBuilder(
+ tagName,
+ TagMode.StartTagAndEndTag,
+ attributes: attributes,
+ children: children))
+ {
+ }
+
+ public MarkupTagHelperBlock(
+ string tagName,
+ TagMode tagMode,
+ IList attributes,
+ params SyntaxTreeNode[] children)
+ : base(new TagHelperBlockBuilder(tagName, tagMode, attributes, children))
+ {
+ }
+ }
+
internal class SectionBlock : Block
{
private const BlockType ThisBlockType = BlockType.Section;
diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CaseSensitiveTagHelperDescriptorComparer.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CaseSensitiveTagHelperDescriptorComparer.cs
new file mode 100644
index 0000000000..263ff352b5
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CaseSensitiveTagHelperDescriptorComparer.cs
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.cs
new file mode 100644
index 0000000000..6fed1a2944
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.cs
@@ -0,0 +1,42 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs
index d530f10811..4c674a48a2 100644
--- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs
+++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs
@@ -282,6 +282,35 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
}
+ private static void EvaluateTagHelperAttribute(
+ ErrorCollector collector,
+ TagHelperAttributeNode actual,
+ TagHelperAttributeNode expected)
+ {
+ if (actual.Name != expected.Name)
+ {
+ collector.AddError("{0} - FAILED :: Attribute names do not match", expected.Name);
+ }
+ else
+ {
+ collector.AddMessage("{0} - PASSED :: Attribute names match", expected.Name);
+ }
+
+ if (actual.ValueStyle != expected.ValueStyle)
+ {
+ collector.AddError("{0} - FAILED :: Attribute value styles do not match", expected.ValueStyle.ToString());
+ }
+ else
+ {
+ collector.AddMessage("{0} - PASSED :: Attribute value style match", expected.ValueStyle);
+ }
+
+ if (actual.ValueStyle != HtmlAttributeValueStyle.Minimized)
+ {
+ EvaluateSyntaxTreeNode(collector, actual.Value, expected.Value);
+ }
+ }
+
private static void EvaluateSyntaxTreeNode(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected)
{
if (actual == null)
@@ -327,6 +356,11 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
else
{
+ if (actual is TagHelperBlock)
+ {
+ EvaluateTagHelperBlock(collector, actual as TagHelperBlock, expected as TagHelperBlock);
+ }
+
AddPassedMessage(collector, expected);
using (collector.Indent())
{
@@ -351,6 +385,50 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
}
+ private static void EvaluateTagHelperBlock(ErrorCollector collector, TagHelperBlock actual, TagHelperBlock expected)
+ {
+ if (expected == null)
+ {
+ AddMismatchError(collector, actual, expected);
+ }
+ else
+ {
+ if (!string.Equals(expected.TagName, actual.TagName, StringComparison.Ordinal))
+ {
+ collector.AddError(
+ "{0} - FAILED :: TagName mismatch for TagHelperBlock :: ACTUAL: {1}",
+ expected.TagName,
+ actual.TagName);
+ }
+
+ if (expected.TagMode != actual.TagMode)
+ {
+ collector.AddError(
+ $"{expected.TagMode} - FAILED :: {nameof(TagMode)} for {nameof(TagHelperBlock)} " +
+ $"{actual.TagName} :: ACTUAL: {actual.TagMode}");
+ }
+
+ var expectedAttributes = expected.Attributes.GetEnumerator();
+ var actualAttributes = actual.Attributes.GetEnumerator();
+
+ while (expectedAttributes.MoveNext())
+ {
+ if (!actualAttributes.MoveNext())
+ {
+ collector.AddError("{0} - FAILED :: No more attributes on this node", expectedAttributes.Current);
+ }
+ else
+ {
+ EvaluateTagHelperAttribute(collector, actualAttributes.Current, expectedAttributes.Current);
+ }
+ }
+ while (actualAttributes.MoveNext())
+ {
+ collector.AddError("End of Attributes - FAILED :: Found Attribute: {0}", actualAttributes.Current.Name);
+ }
+ }
+ }
+
private static void AddPassedMessage(ErrorCollector collector, SyntaxTreeNode expected)
{
collector.AddMessage("{0} - PASSED", expected);
diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperAttributeDescriptorComparer.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperAttributeDescriptorComparer.cs
new file mode 100644
index 0000000000..c50eb84dd1
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperAttributeDescriptorComparer.cs
@@ -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
+ {
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperAttributeDesignTimeDescriptorComparer.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperAttributeDesignTimeDescriptorComparer.cs
new file mode 100644
index 0000000000..c54668476c
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperAttributeDesignTimeDescriptorComparer.cs
@@ -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
+ {
+ 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;
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperBlockRewriterTest.cs
new file mode 100644
index 0000000000..ffd8ca914d
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperBlockRewriterTest.cs
@@ -0,0 +1,4039 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
+{
+ public class TagHelperBlockRewriterTest : TagHelperRewritingTestBase
+ {
+ public static TheoryData SymbolBoundAttributeData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+
+ return new TheoryData
+ {
+ {
+ "
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("ul",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("bound", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("[item]", factory.CodeMarkup("items"), HtmlAttributeValueStyle.SingleQuotes)
+ }))
+ },
+ {
+ "
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("ul",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("bound", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("[(item)]", factory.CodeMarkup("items"), HtmlAttributeValueStyle.SingleQuotes)
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("button",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("bound", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode(
+ "(click)",
+ factory.CodeMarkup("doSomething()"),
+ HtmlAttributeValueStyle.SingleQuotes)
+ },
+ children: factory.Markup("Click Me")))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("button",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("bound", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode(
+ "(^click)",
+ factory.CodeMarkup("doSomething()"),
+ HtmlAttributeValueStyle.SingleQuotes)
+ },
+ children: factory.Markup("Click Me")))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("template",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("bound", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode(
+ "*something",
+ factory.Markup("value"),
+ HtmlAttributeValueStyle.SingleQuotes)
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("div",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("bound", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("#localminimized", null, HtmlAttributeValueStyle.Minimized)
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("div",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("bound", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("#local", factory.Markup("value"), HtmlAttributeValueStyle.SingleQuotes)
+ }))
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(SymbolBoundAttributeData))]
+ public void Rewrite_CanHandleSymbolBoundAttributes(string documentContent, object expectedOutput)
+ {
+ // Arrange
+ var descriptors = new[]
+ {
+ new TagHelperDescriptor
+ {
+ TagName = "*",
+ TypeName = "CatchAllTagHelper",
+ AssemblyName = "SomeAssembly",
+ Attributes = new[]
+ {
+ new TagHelperAttributeDescriptor
+ {
+ Name = "[item]",
+ PropertyName = "ListItems",
+ TypeName = typeof(List).FullName
+ },
+ new TagHelperAttributeDescriptor
+ {
+ Name = "[(item)]",
+ PropertyName = "ArrayItems",
+ TypeName = typeof(string[]).FullName
+ },
+ new TagHelperAttributeDescriptor
+ {
+ Name = "(click)",
+ PropertyName = "Event1",
+ TypeName = typeof(Action).FullName
+ },
+ new TagHelperAttributeDescriptor
+ {
+ Name = "(^click)",
+ PropertyName = "Event2",
+ TypeName = typeof(Action).FullName
+ },
+ new TagHelperAttributeDescriptor
+ {
+ Name = "*something",
+ PropertyName = "StringProperty1",
+ TypeName = typeof(string).FullName
+ },
+ new TagHelperAttributeDescriptor
+ {
+ Name = "#local",
+ PropertyName = "StringProperty2",
+ TypeName = typeof(string).FullName
+ },
+ },
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "bound" } },
+ },
+ };
+ var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
+
+ // Act & Assert
+ EvaluateData(descriptorProvider, documentContent, (MarkupBlock)expectedOutput, expectedErrors: new RazorError[0]);
+ }
+
+ public static TheoryData WithoutEndTagElementData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var blockFactory = new BlockFactory(factory);
+
+ // documentContent, expectedOutput
+ return new TheoryData
+ {
+ {
+ "",
+ new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.StartTagOnly))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.StartTagOnly,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("type", factory.Markup("text"), HtmlAttributeValueStyle.SingleQuotes)
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("input", TagMode.StartTagOnly),
+ new MarkupTagHelperBlock("input", TagMode.StartTagOnly))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.StartTagOnly,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("type", factory.Markup("text"), HtmlAttributeValueStyle.SingleQuotes)
+ }),
+ new MarkupTagHelperBlock("input", TagMode.StartTagOnly))
+ },
+ {
+ "",
+ new MarkupBlock(
+ blockFactory.MarkupTagBlock("
"),
+ new MarkupTagHelperBlock("input", TagMode.StartTagOnly),
+ new MarkupTagHelperBlock("input", TagMode.StartTagOnly),
+ blockFactory.MarkupTagBlock("
"))
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(WithoutEndTagElementData))]
+ public void Rewrite_CanHandleWithoutEndTagTagStructure(string documentContent, object expectedOutput)
+ {
+ // Arrange
+ var descriptors = new TagHelperDescriptor[]
+ {
+ new TagHelperDescriptor
+ {
+ TagName = "input",
+ TypeName = "InputTagHelper",
+ AssemblyName = "SomeAssembly",
+ TagStructure = TagStructure.WithoutEndTag,
+ }
+ };
+ var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
+
+ // Act & Assert
+ EvaluateData(descriptorProvider, documentContent, (MarkupBlock)expectedOutput, expectedErrors: new RazorError[0]);
+ }
+
+ public static TheoryData TagStructureCompatibilityData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var blockFactory = new BlockFactory(factory);
+
+ // documentContent, structure1, structure2, expectedOutput
+ return new TheoryData
+ {
+ {
+ "",
+ TagStructure.Unspecified,
+ TagStructure.Unspecified,
+ new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.StartTagAndEndTag))
+ },
+ {
+ "",
+ TagStructure.Unspecified,
+ TagStructure.Unspecified,
+ new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.SelfClosing))
+ },
+ {
+ "",
+ TagStructure.Unspecified,
+ TagStructure.WithoutEndTag,
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.StartTagOnly,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("type", factory.Markup("text"), HtmlAttributeValueStyle.SingleQuotes)
+ }))
+ },
+ {
+ "",
+ TagStructure.WithoutEndTag,
+ TagStructure.WithoutEndTag,
+ new MarkupBlock(
+ new MarkupTagHelperBlock("input", TagMode.StartTagOnly),
+ new MarkupTagHelperBlock("input", TagMode.StartTagOnly))
+ },
+ {
+ "",
+ TagStructure.Unspecified,
+ TagStructure.NormalOrSelfClosing,
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.StartTagAndEndTag,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("type", factory.Markup("text"), HtmlAttributeValueStyle.SingleQuotes)
+ }))
+ },
+ {
+ "",
+ TagStructure.Unspecified,
+ TagStructure.WithoutEndTag,
+ new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.SelfClosing))
+ },
+
+ {
+ "",
+ TagStructure.NormalOrSelfClosing,
+ TagStructure.Unspecified,
+ new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.SelfClosing))
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TagStructureCompatibilityData))]
+ public void Rewrite_AllowsCompatibleTagStructures(
+ string documentContent,
+ int structure1,
+ int structure2,
+ object expectedOutput)
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ var blockFactory = new BlockFactory(factory);
+ var descriptors = new TagHelperDescriptor[]
+ {
+ new TagHelperDescriptor
+ {
+ TagName = "input",
+ TypeName = "InputTagHelper1",
+ AssemblyName = "SomeAssembly",
+ TagStructure = (TagStructure)structure1
+ },
+ new TagHelperDescriptor
+ {
+ TagName = "input",
+ TypeName = "InputTagHelper2",
+ AssemblyName = "SomeAssembly",
+ TagStructure = (TagStructure)structure2
+ }
+ };
+ var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
+
+ // Act & Assert
+ EvaluateData(descriptorProvider, documentContent, (MarkupBlock)expectedOutput, expectedErrors: new RazorError[0]);
+ }
+
+ public static TheoryData MalformedTagHelperAttributeBlockData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var blockFactory = new BlockFactory(factory);
+ var errorFormatUnclosed = "Found a malformed '{0}' tag helper. Tag helpers must have a start and " +
+ "end tag or be self closing.";
+ var errorFormatNoCloseAngle = "Missing close angle for tag helper '{0}'.";
+ var errorFormatNoCSharp = "The tag helper '{0}' must not have C# in the element's attribute " +
+ "declaration area.";
+ Func createInvalidDoBlock = extraCode =>
+ {
+ return new MarkupBlock(
+ new MarkupBlock(
+ new DynamicAttributeBlockChunkGenerator(
+ new LocationTagged(
+ string.Empty,
+ new SourceLocation(10, 0, 10)),
+ new SourceLocation(10, 0, 10)),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.Code("do {" + extraCode).AsStatement())));
+ };
+
+ return new TheoryData
+ {
+ {
+ "
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode(
+ "bar",
+ new MarkupBlock(factory.Markup("false"), factory.Markup(" ")),
+ HtmlAttributeValueStyle.SingleQuotes)
+ })),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1)
+ }
+ },
+ {
+ "
+ {
+ new TagHelperAttributeNode(
+ "bar",
+ new MarkupBlock(
+ factory.Markup("false"),
+ factory.Markup("
+ {
+ new TagHelperAttributeNode(
+ "bar",
+ factory.Markup("false"),
+ HtmlAttributeValueStyle.DoubleQuotes)
+ })),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ "TagHelper attributes must be well-formed.",
+ new SourceLocation(12, 0, 12),
+ length: 1)
+ }
+ },
+ {
+ "
+ {
+ new TagHelperAttributeNode(
+ "bar",
+ factory.Markup("false'"))
+ })),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1)
+ }
+ },
+ {
+ "
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode(
+ "bar",
+ new MarkupBlock(
+ factory.Markup("false'"),
+ factory.Markup(" >
")))
+ })),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1)
+ }
+ },
+ {
+ "
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("foo", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("bar", null, HtmlAttributeValueStyle.Minimized)
+ },
+ new MarkupTagHelperBlock("strong"))),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "strong"),
+ new SourceLocation(11, 0, 11),
+ length: 6)
+ }
+ },
+ {
+ "
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ })),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1)
+ }
+ },
+ {
+ "
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ })),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1)
+ }
+ },
+ {
+ "
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode(
+ "class",
+ new MarkupBlock(factory.Markup("btn"), factory.Markup(" bar="))),
+ new TagHelperAttributeNode("foo", null, HtmlAttributeValueStyle.Minimized)
+ },
+ new MarkupTagHelperBlock("strong"))),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "strong"),
+ new SourceLocation(24, 0, 24),
+ length: 6)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode(
+ "class",
+ new MarkupBlock(factory.Markup("btn"), factory.Markup(" bar="))),
+ new TagHelperAttributeNode("foo", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new RazorError[0]
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p")),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCSharp, "p"),
+ absoluteIndex: 3, lineIndex: 0 , columnIndex: 3, length: 13)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p")),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCSharp, "p"),
+ absoluteIndex: 3, lineIndex: 0 , columnIndex: 3, length: 13)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode(
+ "class",
+ new MarkupBlock(
+ new MarkupBlock(
+ new DynamicAttributeBlockChunkGenerator(
+ new LocationTagged(
+ string.Empty,
+ new SourceLocation(9, 0, 9)),
+ new SourceLocation(9, 0, 9)),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime.Now")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)))),
+ HtmlAttributeValueStyle.DoubleQuotes)
+ })),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1)
+ }
+ },
+ {
+ "
+ {
+ new TagHelperAttributeNode(
+ "class",
+ createInvalidDoBlock(string.Empty))
+ })),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("do", "}", "{"),
+ absoluteIndex: 11, lineIndex: 0, columnIndex: 11, length: 1)
+ }
+ },
+ {
+ "
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", createInvalidDoBlock("\">
"))
+ })),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("do", "}", "{"),
+ absoluteIndex: 11, lineIndex: 0, columnIndex: 11, length: 1),
+ new RazorError(
+ LegacyResources.ParseError_Unterminated_String_Literal,
+ absoluteIndex: 15, lineIndex: 0, columnIndex: 15, length: 1)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p")),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(1, 0, 1),
+ length: 1),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCSharp, "p"),
+ absoluteIndex: 3, lineIndex: 0 , columnIndex: 3, length: 30),
+ new RazorError(
+ LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("do", "}", "{"),
+ absoluteIndex: 4, lineIndex: 0, columnIndex: 4, length: 1),
+ new RazorError(
+ LegacyResources.FormatParseError_UnexpectedEndTag("p"),
+ absoluteIndex: 31, lineIndex: 0, columnIndex: 31, length: 1)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("some"))
+ })),
+ new []
+ {
+ new RazorError(
+ "TagHelper attributes must be well-formed.",
+ new SourceLocation(13, 0, 13),
+ length: 13)
+ }
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(MalformedTagHelperAttributeBlockData))]
+ public void Rewrite_CreatesErrorForMalformedTagHelpersWithAttributes(
+ string documentContent,
+ object expectedOutput,
+ object expectedErrors)
+ {
+ RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors, "strong", "p");
+ }
+
+ public static TheoryData MalformedTagHelperBlockData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var blockFactory = new BlockFactory(factory);
+ var errorFormatUnclosed = "Found a malformed '{0}' tag helper. Tag helpers must have a start and " +
+ "end tag or be self closing.";
+ var errorFormatNoCloseAngle = "Missing close angle for tag helper '{0}'.";
+
+ return new TheoryData
+ {
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("strong",
+ new MarkupTagHelperBlock("p"))),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "strong"),
+ new SourceLocation(1, 0, 1),
+ length: 6),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "strong"),
+ new SourceLocation(1, 0, 1),
+ length: 6),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(9, 0, 9),
+ length: 1)
+ }
+ },
+ {
+ " <
",
+ new MarkupBlock(
+ blockFactory.MarkupTagBlock("<"),
+ blockFactory.MarkupTagBlock("<"),
+ blockFactory.MarkupTagBlock("
"),
+ factory.Markup(" "),
+ blockFactory.MarkupTagBlock("<"),
+ new MarkupTagHelperBlock("p")),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "strong"),
+ new SourceLocation(4, 0, 4),
+ length: 6),
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(14, 0, 14),
+ length: 1)
+ }
+ },
+ {
+ "<<> <<>>",
+ new MarkupBlock(
+ blockFactory.MarkupTagBlock("<"),
+ blockFactory.MarkupTagBlock("<"),
+ new MarkupTagHelperBlock("strong",
+ factory.Markup("> "),
+ blockFactory.MarkupTagBlock("<"),
+ blockFactory.MarkupTagBlock("<>"),
+ factory.Markup(">"))),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "strong"),
+ new SourceLocation(3, 0, 3),
+ length: 6)
+ }
+ },
+ {
+ "
",
+ new MarkupBlock(
+ blockFactory.MarkupTagBlock(""))),
+ new []
+ {
+ new RazorError(
+ string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
+ new SourceLocation(14, 0, 14),
+ length: 1)
+ }
+ }
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(MalformedTagHelperBlockData))]
+ public void Rewrite_CreatesErrorForMalformedTagHelper(
+ string documentContent,
+ object expectedOutput,
+ object expectedErrors)
+ {
+ RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors, "strong", "p");
+ }
+
+ public static TheoryData CodeTagHelperAttributesData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var dateTimeNow = new MarkupBlock(
+ factory.Markup(" "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime.Now")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)));
+
+ return new TheoryData
+ {
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("person",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("age", factory.CodeMarkup("12"))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("person",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "birthday",
+ factory.CodeMarkup("DateTime.Now"))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("person",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "age",
+ new MarkupBlock(
+ new MarkupBlock(
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory
+ .CSharpCodeMarkup("DateTime.Now.Year")
+ .With(new ExpressionChunkGenerator())))))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("person",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "age",
+ new MarkupBlock(
+ new MarkupBlock(
+ factory.CodeMarkup(" "),
+ new ExpressionBlock(
+ factory.CSharpCodeMarkup("@"),
+ factory
+ .CSharpCodeMarkup("DateTime.Now.Year")
+ .With(new ExpressionChunkGenerator())))))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("person",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("name", factory.Markup("John"))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("person",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "name",
+ new MarkupBlock(factory.Markup("Time:"), dateTimeNow))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("person",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "age",
+ new MarkupBlock(
+ factory.CodeMarkup("1"),
+ factory.CodeMarkup(" +"),
+ new MarkupBlock(
+ factory.CodeMarkup(" "),
+ new ExpressionBlock(
+ factory.CSharpCodeMarkup("@"),
+ factory.CSharpCodeMarkup("value")
+ .With(new ExpressionChunkGenerator()))),
+ factory.CodeMarkup(" +"),
+ factory.CodeMarkup(" 2"))),
+ new TagHelperAttributeNode(
+ "birthday",
+ new MarkupBlock(
+ factory.CodeMarkup("(bool)"),
+ new MarkupBlock(
+ new ExpressionBlock(
+ factory.CSharpCodeMarkup("@"),
+ factory
+ .CSharpCodeMarkup("Bag[\"val\"]")
+ .With(new ExpressionChunkGenerator()))),
+ factory.CodeMarkup(" ?"),
+ new MarkupBlock(
+ factory.CodeMarkup(" @")
+ .As(SpanKind.Code),
+ factory.CodeMarkup("@")
+ .As(SpanKind.Code)
+ .With(SpanChunkGenerator.Null)),
+ factory.CodeMarkup("DateTime"),
+ factory.CodeMarkup(" :"),
+ new MarkupBlock(
+ factory.CodeMarkup(" "),
+ new ExpressionBlock(
+ factory.CSharpCodeMarkup("@"),
+ factory
+ .CSharpCodeMarkup("DateTime.Now")
+ .With(new ExpressionChunkGenerator())))),
+ HtmlAttributeValueStyle.SingleQuotes)
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("person",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("age", factory.CodeMarkup("12")),
+ new TagHelperAttributeNode(
+ "birthday",
+ factory.CodeMarkup("DateTime.Now")),
+ new TagHelperAttributeNode(
+ "name",
+ new MarkupBlock(factory.Markup("Time:"), dateTimeNow))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("person",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("age", factory.CodeMarkup("12")),
+ new TagHelperAttributeNode(
+ "birthday",
+ factory.CodeMarkup("DateTime.Now")),
+ new TagHelperAttributeNode(
+ "name",
+ new MarkupBlock(
+ factory.Markup("Time:"),
+ new MarkupBlock(
+ factory.Markup(" @").Accepts(AcceptedCharacters.None),
+ factory.Markup("@")
+ .With(SpanChunkGenerator.Null)
+ .Accepts(AcceptedCharacters.None)),
+ dateTimeNow))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("person",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("age", factory.CodeMarkup("12")),
+ new TagHelperAttributeNode(
+ "birthday",
+ factory.CodeMarkup("DateTime.Now")),
+ new TagHelperAttributeNode(
+ "name",
+ new MarkupBlock(
+ new MarkupBlock(
+ factory.Markup("@").Accepts(AcceptedCharacters.None),
+ factory.Markup("@")
+ .With(SpanChunkGenerator.Null)
+ .Accepts(AcceptedCharacters.None)),
+ factory.Markup("BoundStringAttribute")))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("person",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "age",
+ new MarkupBlock(
+ new MarkupBlock(
+ factory.CodeMarkup("@"),
+ factory.CodeMarkup("@")
+ .With(SpanChunkGenerator.Null)),
+ new MarkupBlock(
+ factory.EmptyHtml()
+ .AsCodeMarkup()
+ .As(SpanKind.Code),
+ new ExpressionBlock(
+ factory.CSharpCodeMarkup("@"),
+ factory.CSharpCodeMarkup("("),
+ factory.CSharpCodeMarkup("11+1")
+ .With(new ExpressionChunkGenerator()),
+ factory.CSharpCodeMarkup(")"))))),
+ new TagHelperAttributeNode(
+ "birthday",
+ factory.CodeMarkup("DateTime.Now")),
+ new TagHelperAttributeNode(
+ "name",
+ new MarkupBlock(factory.Markup("Time:"), dateTimeNow))
+ }))
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(CodeTagHelperAttributesData))]
+ public void TagHelperParseTreeRewriter_CreatesMarkupCodeSpansForNonStringTagHelperAttributes(
+ string documentContent,
+ object expectedOutput)
+ {
+ // Arrange
+ var descriptors = new TagHelperDescriptor[]
+ {
+ new TagHelperDescriptor
+ {
+ TagName = "person",
+ TypeName = "PersonTagHelper",
+ AssemblyName = "personAssembly",
+ Attributes = new[]
+ {
+ new TagHelperAttributeDescriptor
+ {
+ Name = "age",
+ PropertyName = "Age",
+ TypeName = typeof(int).FullName
+ },
+ new TagHelperAttributeDescriptor
+ {
+ Name = "birthday",
+ PropertyName = "BirthDay",
+ TypeName = typeof(DateTime).FullName
+ },
+ new TagHelperAttributeDescriptor
+ {
+ Name = "name",
+ PropertyName = "Name",
+ TypeName = typeof(string).FullName,
+ IsStringProperty = true
+ }
+ }
+ }
+ };
+ var providerContext = new TagHelperDescriptorProvider(descriptors);
+
+ // Act & Assert
+ EvaluateData(providerContext,
+ documentContent,
+ (MarkupBlock)expectedOutput,
+ expectedErrors: Enumerable.Empty());
+ }
+
+ public static IEnumerable
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("script",
+ factory.Markup("Hel
lo
")),
+ factory.Markup(" "),
+ new MarkupTagHelperBlock("p",
+ new MarkupTagHelperBlock("div",
+ factory.Markup("World"))))
+ };
+ yield return new object[]
+ {
+ " ",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("script",
+ factory.Markup("Hello")),
+ factory.Markup(" "),
+ new MarkupTagHelperBlock("script",
+ factory.Markup("World")))
+ };
+ yield return new object[]
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("script",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ }))
+ };
+ yield return new object[]
+ {
+ "
Hello World
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ factory.Markup("Hello "),
+ new MarkupTagHelperBlock("script",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ }),
+ factory.Markup(" World")))
+ };
+ yield return new object[]
+ {
+ "
Hello World
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ factory.Markup("Hello "),
+ new MarkupTagHelperBlock("script",
+ new List
+ {
+ new TagHelperAttributeNode(
+ "class",
+ new MarkupBlock(
+ new MarkupBlock(
+ factory.Markup("@").Accepts(AcceptedCharacters.None),
+ factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)),
+ factory.Markup("foo@bar.com"))),
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ }),
+ factory.Markup(" World")))
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ScriptBlockData))]
+ public void TagHelperParseTreeRewriter_RewritesScriptTagHelpers(
+ string documentContent,
+ object expectedOutput)
+ {
+ RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p", "div", "script");
+ }
+
+ public static IEnumerable SelfClosingBlockData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+
+ yield return new object[]
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ }))
+ };
+ yield return new object[]
+ {
+ "
Hello
World
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.StartTagAndEndTag,
+ children: new SyntaxTreeNode[]
+ {
+ factory.Markup("Hello "),
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode(
+ "style",
+ factory.Markup("color:red;"))
+ }),
+ factory.Markup(" World")
+ }))
+ };
+ yield return new object[]
+ {
+ "Hello World",
+ new MarkupBlock(
+ factory.Markup("Hello"),
+ new MarkupTagHelperBlock("p",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo"))
+ }),
+ factory.Markup(" "),
+ new MarkupTagHelperBlock("p",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ }),
+ factory.Markup("World"))
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(SelfClosingBlockData))]
+ public void TagHelperParseTreeRewriter_RewritesSelfClosingTagHelpers(
+ string documentContent,
+ object expectedOutput)
+ {
+ RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p");
+ }
+
+ public static IEnumerable QuotelessAttributeBlockData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var blockFactory = new BlockFactory(factory);
+ var dateTimeNow = new Func(index =>
+ new MarkupBlock(
+ new MarkupBlock(
+ new DynamicAttributeBlockChunkGenerator(
+ new LocationTagged(
+ string.Empty,
+ new SourceLocation(index, 0, index)),
+ new SourceLocation(index, 0, index)),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime.Now")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)))));
+
+ yield return new object[]
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode("dynamic", dateTimeNow(21)),
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ }))
+ };
+ yield return new object[]
+ {
+ "
Hello World
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode("dynamic", dateTimeNow(21)),
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ },
+ factory.Markup("Hello World")))
+ };
+ yield return new object[]
+ {
+ "
Hello World
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode("dynamic", dateTimeNow(21)),
+ new TagHelperAttributeNode(
+ "style",
+ new MarkupBlock(
+ factory.Markup("color"),
+ new MarkupBlock(
+ factory.Markup("@").Accepts(AcceptedCharacters.None),
+ factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)),
+ factory.Markup(":red;")),
+ HtmlAttributeValueStyle.DoubleQuotes)
+ },
+ factory.Markup("Hello World")))
+ };
+ yield return new object[]
+ {
+ "
Hello
World
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode("dynamic", dateTimeNow(21))
+ },
+ factory.Markup("Hello")),
+ factory.Markup(" "),
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("style", factory.Markup("color:red;")),
+ new TagHelperAttributeNode("dynamic", dateTimeNow(73))
+ },
+ factory.Markup("World")))
+ };
+ yield return new object[]
+ {
+ "
Hello World inside of strong tag
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode("dynamic", dateTimeNow(21)),
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ },
+ factory.Markup("Hello World "),
+ new MarkupTagBlock(
+ factory.Markup("(" class=\"", 71, 0, 71),
+ suffix: new LocationTagged("\"", 82, 0, 82)),
+ factory.Markup(" class=\"").With(SpanChunkGenerator.Null),
+ factory.Markup("foo").With(new LiteralAttributeChunkGenerator(prefix: new LocationTagged(string.Empty, 79, 0, 79),
+ value: new LocationTagged("foo", 79, 0, 79))),
+ factory.Markup("\"").With(SpanChunkGenerator.Null)),
+ factory.Markup(">")),
+ factory.Markup("inside of strong tag"),
+ blockFactory.MarkupTagBlock("")))
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(QuotelessAttributeBlockData))]
+ public void TagHelperParseTreeRewriter_RewritesTagHelpersWithQuotelessAttributes(
+ string documentContent,
+ object expectedOutput)
+ {
+ RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p");
+ }
+
+ public static IEnumerable PlainAttributeBlockData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var blockFactory = new BlockFactory(factory);
+
+ yield return new object[]
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ }))
+ };
+ yield return new object[]
+ {
+ "
Hello World
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ },
+ factory.Markup("Hello World")))
+ };
+ yield return new object[]
+ {
+ "
Hello
World
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo"))
+ },
+ factory.Markup("Hello")),
+ factory.Markup(" "),
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ },
+ factory.Markup("World")))
+ };
+ yield return new object[]
+ {
+ "
Hello World inside of strong tag
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("foo")),
+ new TagHelperAttributeNode("style", factory.Markup("color:red;"))
+ },
+ factory.Markup("Hello World "),
+ new MarkupTagBlock(
+ factory.Markup("(" class=\"", 53, 0, 53),
+ suffix: new LocationTagged("\"", 64, 0, 64)),
+ factory.Markup(" class=\"").With(SpanChunkGenerator.Null),
+ factory.Markup("foo").With(new LiteralAttributeChunkGenerator(prefix: new LocationTagged(string.Empty, 61, 0, 61),
+ value: new LocationTagged("foo", 61, 0, 61))),
+ factory.Markup("\"").With(SpanChunkGenerator.Null)),
+ factory.Markup(">")),
+ factory.Markup("inside of strong tag"),
+ blockFactory.MarkupTagBlock("")))
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(PlainAttributeBlockData))]
+ public void TagHelperParseTreeRewriter_RewritesTagHelpersWithPlainAttributes(
+ string documentContent,
+ object expectedOutput)
+ {
+ RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p");
+ }
+
+ public static IEnumerable PlainBlockData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var blockFactory = new BlockFactory(factory);
+
+ yield return new object[]
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p"))
+ };
+ yield return new object[]
+ {
+ "
Hello World
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ factory.Markup("Hello World")))
+ };
+ yield return new object[]
+ {
+ "
Hello
World
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ factory.Markup("Hello")),
+ factory.Markup(" "),
+ new MarkupTagHelperBlock("p",
+ factory.Markup("World")))
+ };
+ yield return new object[]
+ {
+ "
Hello World inside of strong tag
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ factory.Markup("Hello World "),
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup("inside of strong tag"),
+ blockFactory.MarkupTagBlock("")))
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(PlainBlockData))]
+ public void TagHelperParseTreeRewriter_RewritesPlainTagHelperTagBlocks(
+ string documentContent,
+ object expectedOutput)
+ {
+ RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p");
+ }
+
+ public static TheoryData DataDashAttributeData_Document
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var dateTimeNowString = "@DateTime.Now";
+ var dateTimeNow = new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime.Now")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace));
+
+ // documentContent, expectedOutput
+ return new TheoryData
+ {
+ {
+ $"",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode(
+ "data-required",
+ new MarkupBlock(dateTimeNow),
+ HtmlAttributeValueStyle.SingleQuotes),
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("data-required", factory.Markup("value"), HtmlAttributeValueStyle.SingleQuotes),
+ }))
+ },
+ {
+ $"",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode(
+ "data-required",
+ new MarkupBlock(factory.Markup("prefix "), dateTimeNow),
+ HtmlAttributeValueStyle.SingleQuotes),
+ }))
+ },
+ {
+ $"",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode(
+ "data-required",
+ new MarkupBlock(dateTimeNow, factory.Markup(" suffix")),
+ HtmlAttributeValueStyle.SingleQuotes),
+ }))
+ },
+ {
+ $"",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode(
+ "data-required",
+ new MarkupBlock(
+ factory.Markup("prefix "),
+ dateTimeNow,
+ factory.Markup(" suffix")),
+ HtmlAttributeValueStyle.SingleQuotes),
+ }))
+ },
+ {
+ $"",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("pre-attribute", value: null, valueStyle: HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode(
+ "data-required",
+ new MarkupBlock(
+ factory.Markup("prefix "),
+ dateTimeNow,
+ factory.Markup(" suffix")),
+ HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("post-attribute", value: null, valueStyle: HtmlAttributeValueStyle.Minimized),
+ }))
+ },
+ {
+ $"",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode(
+ "data-required",
+ new MarkupBlock(
+ dateTimeNow,
+ factory.Markup(" middle "),
+ dateTimeNow),
+ HtmlAttributeValueStyle.SingleQuotes),
+ }))
+ },
+ };
+ }
+ }
+
+ public static TheoryData DataDashAttributeData_CSharpBlock
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var documentData = DataDashAttributeData_Document;
+ Func, MarkupBlock> buildStatementBlock = (insideBuilder) =>
+ {
+ return new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ insideBuilder(),
+ factory.EmptyCSharp().AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ factory.EmptyHtml());
+ };
+
+ foreach (var data in documentData)
+ {
+ data[0] = $"@{{{data[0]}}}";
+ data[1] = buildStatementBlock(() => data[1] as MarkupBlock);
+ }
+
+ return documentData;
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(DataDashAttributeData_Document))]
+ [MemberData(nameof(DataDashAttributeData_CSharpBlock))]
+ public void Rewrite_GeneratesExpectedOutputForUnboundDataDashAttributes(
+ string documentContent,
+ object expectedOutput)
+ {
+ // Act & Assert
+ RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, Enumerable.Empty(), "input");
+ }
+
+ public static TheoryData MinimizedAttributeData_Document
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var noErrors = new RazorError[0];
+ var errorFormat = "Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound " +
+ "attributes of type '{2}' cannot be empty or contain only whitespace.";
+ var emptyKeyFormat = "The tag helper attribute '{0}' in element '{1}' is missing a key. The " +
+ "syntax is '<{1} {0}{{ key }}=\"value\">'.";
+ var stringType = typeof(string).FullName;
+ var intType = typeof(int).FullName;
+ var expressionString = "@DateTime.Now + 1";
+ var expression = new Func(index =>
+ new MarkupBlock(
+ new MarkupBlock(
+ new DynamicAttributeBlockChunkGenerator(
+ new LocationTagged(
+ string.Empty,
+ new SourceLocation(index, 0, index)),
+ new SourceLocation(index, 0, index)),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime.Now")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ factory.Markup(" +")
+ .With(new LiteralAttributeChunkGenerator(
+ prefix: new LocationTagged(" ", index + 13, 0, index + 13),
+ value: new LocationTagged("+", index + 14, 0, index + 14))),
+ factory.Markup(" 1")
+ .With(new LiteralAttributeChunkGenerator(
+ prefix: new LocationTagged(" ", index + 15, 0, index + 15),
+ value: new LocationTagged("1", index + 16, 0, index + 16)))));
+
+ // documentContent, expectedOutput, expectedErrors
+ return new TheoryData
+ {
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("unbound-required", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ noErrors
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.StartTagAndEndTag,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-string", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-string", "p", stringType), 3, 0, 3, 12)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-required-string", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-required-string", "input", stringType), 7, 0, 7, 21)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-required-int", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-required-int", "input", intType), 7, 0, 7, 18)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.StartTagAndEndTag,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-int", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[] { new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 3, 0, 3, 9) }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("int-dictionary", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "int-dictionary", "input", typeof(IDictionary).FullName),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 14),
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("string-dictionary", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "string-dictionary", "input", typeof(IDictionary).FullName),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 17),
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("int-prefix-", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "int-prefix-", "input", typeof(int).FullName),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 11),
+ new RazorError(
+ string.Format(emptyKeyFormat, "int-prefix-", "input"),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 11),
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("string-prefix-", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "string-prefix-", "input", typeof(string).FullName),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 14),
+ new RazorError(
+ string.Format(emptyKeyFormat, "string-prefix-", "input"),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 14),
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("int-prefix-value", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "int-prefix-value", "input", typeof(int).FullName),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 16),
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("string-prefix-value", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "string-prefix-value", "input", typeof(string).FullName),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 19),
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("int-prefix-value", new MarkupBlock(), HtmlAttributeValueStyle.SingleQuotes),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "int-prefix-value", "input", typeof(int).FullName),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 16),
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("string-prefix-value", new MarkupBlock(), HtmlAttributeValueStyle.SingleQuotes),
+ })),
+ new RazorError[0]
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("int-prefix-value", factory.CodeMarkup("3"), HtmlAttributeValueStyle.SingleQuotes),
+ })),
+ new RazorError[0]
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "string-prefix-value",
+ new MarkupBlock(
+ factory.Markup("some"),
+ factory.Markup(" string")),
+ HtmlAttributeValueStyle.SingleQuotes),
+ })),
+ new RazorError[0]
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("unbound-required", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("bound-required-string", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-required-string", "input", stringType),
+ absoluteIndex: 24,
+ lineIndex: 0,
+ columnIndex: 24,
+ length: 21)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.StartTagAndEndTag,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-int", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("bound-string", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 3, 0, 3, 9),
+ new RazorError(string.Format(errorFormat, "bound-string", "p", stringType), 13, 0, 13, 12),
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-required-int", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("unbound-required", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("bound-required-string", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-required-int", "input", intType), 7, 0, 7, 18),
+ new RazorError(
+ string.Format(errorFormat, "bound-required-string", "input", stringType),
+ absoluteIndex: 43,
+ lineIndex: 0,
+ columnIndex: 43,
+ length: 21)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.StartTagAndEndTag,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-int", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("bound-string", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("bound-string", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 3, 0, 3, 9),
+ new RazorError(string.Format(errorFormat, "bound-string", "p", stringType), 13, 0, 13, 12),
+ new RazorError(string.Format(errorFormat, "bound-string", "p", stringType), 26, 0, 26, 12),
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("unbound-required", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("class", factory.Markup("btn"), HtmlAttributeValueStyle.SingleQuotes),
+ })),
+ noErrors
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.StartTagAndEndTag,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-string", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("class", factory.Markup("btn"), HtmlAttributeValueStyle.SingleQuotes),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-string", "p", stringType),
+ absoluteIndex: 3,
+ lineIndex: 0,
+ columnIndex: 3,
+ length: 12)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"), HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("unbound-required", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ noErrors
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.StartTagAndEndTag,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"), HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("bound-string", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-string", "p", stringType),
+ absoluteIndex: 15,
+ lineIndex: 0,
+ columnIndex: 15,
+ length: 12)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-required-string", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("class", factory.Markup("btn"), HtmlAttributeValueStyle.SingleQuotes),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-required-string", "input", stringType),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 21)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"), HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("bound-required-string", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-required-string", "input", stringType),
+ absoluteIndex: 19,
+ lineIndex: 0,
+ columnIndex: 19,
+ length: 21)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-required-int", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("class", factory.Markup("btn"), HtmlAttributeValueStyle.SingleQuotes),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-required-int", "input", intType), 7, 0, 7, 18)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.StartTagAndEndTag,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-int", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("class", factory.Markup("btn"), HtmlAttributeValueStyle.SingleQuotes),
+ })),
+ new[]
+ {
+ new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 3, 0, 3, 9)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"), HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("bound-required-int", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(string.Format(errorFormat, "bound-required-int", "input", intType), 19, 0, 19, 18)
+ }
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.StartTagAndEndTag,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"), HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("bound-int", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 15, 0, 15, 9)
+ }
+ },
+ {
+ $"",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("class", expression(14), HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("bound-required-int", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-required-int", "input", intType), 33, 0, 33, 18)
+ }
+ },
+ {
+ $"",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.StartTagAndEndTag,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("class", expression(10), HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("bound-int", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 29, 0, 29, 9)
+ }
+ },
+ {
+ $"",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-required-int", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("class", expression(36), HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("bound-required-string", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("class", expression(86), HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("unbound-required", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormat, "bound-required-int", "input", intType), 10, 0, 10, 18),
+ new RazorError(
+ string.Format(errorFormat, "bound-required-string", "input", stringType),
+ absoluteIndex: 57,
+ lineIndex: 0,
+ columnIndex: 57,
+ length: 21),
+ }
+ },
+ {
+ $"",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ TagMode.StartTagAndEndTag,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("bound-int", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("class", expression(23), HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("bound-string", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("class", expression(64), HtmlAttributeValueStyle.SingleQuotes),
+ new TagHelperAttributeNode("bound-string", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(string.Format(errorFormat, "bound-int", "p", intType), 6, 0, 6, 9),
+ new RazorError(string.Format(errorFormat, "bound-string", "p", stringType), 44, 0, 44, 12),
+ new RazorError(string.Format(errorFormat, "bound-string", "p", stringType), 84, 0, 84, 12),
+ }
+ },
+ };
+ }
+ }
+
+ public static TheoryData MinimizedAttributeData_CSharpBlock
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var documentData = MinimizedAttributeData_Document;
+ Func, MarkupBlock> buildStatementBlock = (insideBuilder) =>
+ {
+ return new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ insideBuilder(),
+ factory.EmptyCSharp().AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ factory.EmptyHtml());
+ };
+ Action updateDynamicChunkGenerators = (block) =>
+ {
+ var tagHelperBlock = block.Children.First() as MarkupTagHelperBlock;
+
+ for (var i = 0; i < tagHelperBlock.Attributes.Count; i++)
+ {
+ var attribute = tagHelperBlock.Attributes[i];
+ var holderBlock = attribute.Value as Block;
+
+ if (holderBlock == null)
+ {
+ continue;
+ }
+
+ var valueBlock = holderBlock.Children.FirstOrDefault() as Block;
+ if (valueBlock != null)
+ {
+ var chunkGenerator = valueBlock.ChunkGenerator as DynamicAttributeBlockChunkGenerator;
+
+ if (chunkGenerator != null)
+ {
+ var blockBuilder = new BlockBuilder(holderBlock);
+ var expressionBlockBuilder = new BlockBuilder(valueBlock);
+ var newChunkGenerator = new DynamicAttributeBlockChunkGenerator(
+ new LocationTagged(
+ chunkGenerator.Prefix.Value,
+ new SourceLocation(
+ chunkGenerator.Prefix.Location.AbsoluteIndex + 2,
+ chunkGenerator.Prefix.Location.LineIndex,
+ chunkGenerator.Prefix.Location.CharacterIndex + 2)),
+ new SourceLocation(
+ chunkGenerator.ValueStart.AbsoluteIndex + 2,
+ chunkGenerator.ValueStart.LineIndex,
+ chunkGenerator.ValueStart.CharacterIndex + 2));
+
+ expressionBlockBuilder.ChunkGenerator = newChunkGenerator;
+ blockBuilder.Children[0] = expressionBlockBuilder.Build();
+
+ for (var j = 1; j < blockBuilder.Children.Count; j++)
+ {
+ var span = blockBuilder.Children[j] as Span;
+ if (span != null)
+ {
+ var literalChunkGenerator =
+ span.ChunkGenerator as LiteralAttributeChunkGenerator;
+
+ var spanBuilder = new SpanBuilder(span);
+ spanBuilder.ChunkGenerator = new LiteralAttributeChunkGenerator(
+ prefix: new LocationTagged(
+ literalChunkGenerator.Prefix.Value,
+ new SourceLocation(
+ literalChunkGenerator.Prefix.Location.AbsoluteIndex + 2,
+ literalChunkGenerator.Prefix.Location.LineIndex,
+ literalChunkGenerator.Prefix.Location.CharacterIndex + 2)),
+ value: new LocationTagged(
+ literalChunkGenerator.Value.Value,
+ new SourceLocation(
+ literalChunkGenerator.Value.Location.AbsoluteIndex + 2,
+ literalChunkGenerator.Value.Location.LineIndex,
+ literalChunkGenerator.Value.Location.CharacterIndex + 2)));
+
+ blockBuilder.Children[j] = spanBuilder.Build();
+ }
+ }
+
+ tagHelperBlock.Attributes[i] = new TagHelperAttributeNode(
+ attribute.Name,
+ blockBuilder.Build(),
+ attribute.ValueStyle);
+ }
+ }
+ }
+ };
+
+ foreach (var data in documentData)
+ {
+ data[0] = $"@{{{data[0]}}}";
+
+ updateDynamicChunkGenerators(data[1] as MarkupBlock);
+
+ data[1] = buildStatementBlock(() => data[1] as MarkupBlock);
+
+ var errors = data[2] as RazorError[];
+
+ for (var i = 0; i < errors.Length; i++)
+ {
+ var error = errors[i];
+ error.Location = SourceLocation.Advance(error.Location, "@{");
+ }
+ }
+
+ return documentData;
+ }
+ }
+
+ public static TheoryData MinimizedAttributeData_PartialTags
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var noErrors = new RazorError[0];
+ var errorFormatUnclosed = "Found a malformed '{0}' tag helper. Tag helpers must have a start and " +
+ "end tag or be self closing.";
+ var errorFormatNoCloseAngle = "Missing close angle for tag helper '{0}'.";
+ var errorFormatNoValue = "Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound " +
+ "attributes of type '{2}' cannot be empty or contain only whitespace.";
+ var stringType = typeof(string).FullName;
+ var intType = typeof(int).FullName;
+
+ // documentContent, expectedOutput, expectedErrors
+ return new TheoryData
+ {
+ {
+ "()
+ {
+ new TagHelperAttributeNode("unbound-required", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormatNoCloseAngle, "input"),
+ new SourceLocation(1, 0, 1),
+ length: 5),
+ new RazorError(
+ string.Format(errorFormatUnclosed, "input"),
+ new SourceLocation(1, 0, 1),
+ length: 5),
+ }
+ },
+ {
+ "()
+ {
+ new TagHelperAttributeNode("bound-required-string", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormatNoCloseAngle, "input"),
+ new SourceLocation(1, 0, 1),
+ length: 5),
+ new RazorError(
+ string.Format(errorFormatUnclosed, "input"),
+ new SourceLocation(1, 0, 1),
+ length: 5),
+ new RazorError(
+ string.Format(errorFormatNoValue, "bound-required-string", "input", stringType),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 21),
+ }
+ },
+ {
+ "()
+ {
+ new TagHelperAttributeNode("bound-required-int", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormatNoCloseAngle, "input"),
+ new SourceLocation(1, 0, 1),
+ length: 5),
+ new RazorError(
+ string.Format(errorFormatUnclosed, "input"),
+ new SourceLocation(1, 0, 1),
+ length: 5),
+ new RazorError(
+ string.Format(errorFormatNoValue, "bound-required-int", "input", intType),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 18),
+ }
+ },
+ {
+ "()
+ {
+ new TagHelperAttributeNode("bound-required-int", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("unbound-required", null, HtmlAttributeValueStyle.Minimized),
+ new TagHelperAttributeNode("bound-required-string", null, HtmlAttributeValueStyle.Minimized),
+ })),
+ new[]
+ {
+ new RazorError(
+ string.Format(errorFormatNoCloseAngle, "input"),
+ new SourceLocation(1, 0, 1),
+ length: 5),
+ new RazorError(
+ string.Format(errorFormatUnclosed, "input"),
+ new SourceLocation(1, 0, 1),
+ length: 5),
+ new RazorError(
+ string.Format(errorFormatNoValue, "bound-required-int", "input", intType),
+ absoluteIndex: 7,
+ lineIndex: 0,
+ columnIndex: 7,
+ length: 18),
+ new RazorError(
+ string.Format(errorFormatNoValue, "bound-required-string", "input", stringType),
+ absoluteIndex: 43,
+ lineIndex: 0,
+ columnIndex: 43,
+ length: 21),
+ }
+ },
+ {
+ "
"))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ tagMode: TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ tagMode: TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", dateTimeNow(10))
+ }))
+ },
+ {
+ "
words and spaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ },
+ children: factory.Markup("words and spaces")))
+ },
+ {
+ "
words and spaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", dateTimeNow(10))
+ },
+ children: factory.Markup("words and spaces")))
+ },
+ {
+ "
wordsandspaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ },
+ children: new SyntaxTreeNode[]
+ {
+ factory.Markup("words"),
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup("and"),
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup("spaces")
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "strong",
+ tagMode: TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("catchAll", factory.Markup("hi"))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "strong",
+ tagMode: TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("catchAll", dateTimeNow(18))
+ }))
+ },
+ {
+ "words and spaces",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "strong",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("catchAll", factory.Markup("hi"))
+ },
+ children: factory.Markup("words and spaces")))
+ },
+ {
+ "words and spaces",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "strong",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("catchAll", dateTimeNow(18))
+ },
+ children: factory.Markup("words and spaces")))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagBlock(
+ factory.Markup("
(" class=\"", 4, 0, 4),
+ suffix: new LocationTagged("\"", 15, 0, 15)),
+ factory.Markup(" class=\"").With(SpanChunkGenerator.Null),
+ factory.Markup("btn").With(
+ new LiteralAttributeChunkGenerator(
+ prefix: new LocationTagged(string.Empty, 12, 0, 12),
+ value: new LocationTagged("btn", 12, 0, 12))),
+ factory.Markup("\"").With(SpanChunkGenerator.Null)),
+ factory.Markup(" />")))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagBlock(
+ factory.Markup("
(" class=\"", 4, 0, 4),
+ suffix: new LocationTagged("\"", 15, 0, 15)),
+ factory.Markup(" class=\"").With(SpanChunkGenerator.Null),
+ factory.Markup("btn").With(
+ new LiteralAttributeChunkGenerator(
+ prefix: new LocationTagged(string.Empty, 12, 0, 12),
+ value: new LocationTagged("btn", 12, 0, 12))),
+ factory.Markup("\"").With(SpanChunkGenerator.Null)),
+ factory.Markup(">")),
+ blockFactory.MarkupTagBlock("
"))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ tagMode: TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("notRequired", factory.Markup("a")),
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ tagMode: TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("notRequired", dateTimeNow(16)),
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ }))
+ },
+ {
+ "
words and spaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("notRequired", factory.Markup("a")),
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ },
+ children: factory.Markup("words and spaces")))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "div",
+ tagMode: TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("style", new MarkupBlock()),
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "div",
+ tagMode: TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("style", dateTimeNow(12)),
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ }))
+ },
+ {
+ "
words and spaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "div",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("style", new MarkupBlock()),
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ },
+ children: factory.Markup("words and spaces")))
+ },
+ {
+ "
words and spaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "div",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("style", dateTimeNow(12)),
+ new TagHelperAttributeNode("class", dateTimeNow(34))
+ },
+ children: factory.Markup("words and spaces")))
+ },
+ {
+ "
wordsandspaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "div",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("style", new MarkupBlock()),
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ },
+ children: new SyntaxTreeNode[]
+ {
+ factory.Markup("words"),
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup("and"),
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup("spaces")
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ tagMode: TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn")),
+ new TagHelperAttributeNode("catchAll", factory.Markup("hi"))
+ }))
+ },
+ {
+ "
words and spaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn")),
+ new TagHelperAttributeNode("catchAll", factory.Markup("hi"))
+ },
+ children: factory.Markup("words and spaces")))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "div",
+ tagMode: TagMode.SelfClosing,
+ attributes: new List
+ {
+ new TagHelperAttributeNode("style", new MarkupBlock()),
+ new TagHelperAttributeNode("class", factory.Markup("btn")),
+ new TagHelperAttributeNode("catchAll", factory.Markup("hi"))
+ }))
+ },
+ {
+ "
words and spaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "div",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("style", new MarkupBlock()),
+ new TagHelperAttributeNode("class", factory.Markup("btn")),
+ new TagHelperAttributeNode("catchAll", factory.Markup("hi"))
+ },
+ children: factory.Markup("words and spaces")))
+ },
+ {
+ "
words and spaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "div",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("style", new MarkupBlock()),
+ new TagHelperAttributeNode("class", factory.Markup("btn")),
+ new TagHelperAttributeNode("catchAll",
+ new MarkupBlock(
+ new MarkupBlock(
+ factory.Markup("@").Accepts(AcceptedCharacters.None),
+ factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)),
+ factory.Markup("hi"))),
+ },
+ children: factory.Markup("words and spaces")))
+ },
+ {
+ "
words and " +
+ "spaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "div",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("style", dateTimeNow(12)),
+ new TagHelperAttributeNode("class", dateTimeNow(34)),
+ new TagHelperAttributeNode("catchAll", dateTimeNow(59))
+ },
+ children: factory.Markup("words and spaces")))
+ },
+ {
+ "
wordsandspaces
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "div",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("style", new MarkupBlock()),
+ new TagHelperAttributeNode("class", factory.Markup("btn")),
+ new TagHelperAttributeNode("catchAll", factory.Markup("hi"))
+ },
+ children: new SyntaxTreeNode[]
+ {
+ factory.Markup("words"),
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup("and"),
+ blockFactory.MarkupTagBlock(""),
+ factory.Markup("spaces")
+ }))
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(RequiredAttributeData))]
+ public void Rewrite_RequiredAttributeDescriptorsCreateTagHelperBlocksCorrectly(
+ string documentContent,
+ object expectedOutput)
+ {
+ // Arrange
+ var descriptors = new TagHelperDescriptor[]
+ {
+ new TagHelperDescriptor
+ {
+ TagName = "p",
+ TypeName = "pTagHelper",
+ AssemblyName = "SomeAssembly",
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } }
+ },
+ new TagHelperDescriptor
+ {
+ TagName = "div",
+ TypeName = "divTagHelper",
+ AssemblyName = "SomeAssembly",
+ RequiredAttributes = new[]
+ {
+ new TagHelperRequiredAttributeDescriptor { Name = "class" },
+ new TagHelperRequiredAttributeDescriptor { Name = "style" }
+ }
+ },
+ new TagHelperDescriptor
+ {
+ TagName = "*",
+ TypeName = "catchAllTagHelper",
+ AssemblyName = "SomeAssembly",
+ RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "catchAll" } }
+ }
+ };
+ var descriptorProvider = new TagHelperDescriptorProvider(descriptors);
+
+ // Act & Assert
+ EvaluateData(descriptorProvider, documentContent, (MarkupBlock)expectedOutput, expectedErrors: new RazorError[0]);
+ }
+
+ public static TheoryData NestedRequiredAttributeData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+ var blockFactory = new BlockFactory(factory);
+ var dateTimeNow = new MarkupBlock(
+ new MarkupBlock(
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime.Now")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))));
+
+ // documentContent, expectedOutput
+ return new TheoryData
+ {
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ },
+ children: new[]
+ {
+ blockFactory.MarkupTagBlock("
"),
+ blockFactory.MarkupTagBlock("
")
+ }))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "strong",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("catchAll", factory.Markup("hi"))
+ },
+ children: new SyntaxTreeNode[]
+ {
+ blockFactory.MarkupTagBlock(""),
+ blockFactory.MarkupTagBlock(""),
+ }))
+ },
+ {
+ "
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ },
+ children: new[]
+ {
+ blockFactory.MarkupTagBlock(""),
+ blockFactory.MarkupTagBlock("
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "strong",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("catchAll", factory.Markup("hi"))
+ },
+ children: new SyntaxTreeNode[]
+ {
+ blockFactory.MarkupTagBlock("
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ },
+ children: blockFactory.MarkupTagBlock("
",
+ new MarkupBlock(
+ new MarkupTagHelperBlock("p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode("notRequired", factory.Markup("hi")),
+ new TagHelperAttributeNode("class", factory.Markup("btn"))
+ },
+ children: blockFactory.MarkupTagBlock("