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, "> 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 ' 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) ? " 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 IncompleteHelperBlockData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var malformedErrorFormat = "Found a malformed '{0}' tag helper. Tag helpers must have a start and " + + "end tag or be self closing."; + + yield return new object[] + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", factory.Markup("foo")), + new TagHelperAttributeNode( + "dynamic", + new MarkupBlock( + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator( + new LocationTagged( + string.Empty, + new SourceLocation(21, 0, 21)), + new SourceLocation(21, 0, 21)), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)))), + HtmlAttributeValueStyle.DoubleQuotes), + new TagHelperAttributeNode("style", factory.Markup("color:red;")) + }, + new MarkupTagHelperBlock("strong")), + blockFactory.MarkupTagBlock("
      ")), + new RazorError[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "strong"), + absoluteIndex: 53, lineIndex: 0, columnIndex: 53, length: 6), + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "strong"), + absoluteIndex: 66, lineIndex: 0, columnIndex: 66, length: 6) + } + }; + yield return new object[] + { + "

      Hello World

      ", + new MarkupBlock( + blockFactory.MarkupTagBlock("
      "), + new MarkupTagHelperBlock("p", + factory.Markup("Hello "), + new MarkupTagHelperBlock("strong", + factory.Markup("World")), + blockFactory.MarkupTagBlock("
      "))), + new RazorError[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "p"), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 1) + } + }; + yield return new object[] + { + "

      Hello World

      ", + new MarkupBlock( + blockFactory.MarkupTagBlock("
      "), + new MarkupTagHelperBlock("p", + factory.Markup("Hello "), + new MarkupTagHelperBlock("strong", + factory.Markup("World"), + blockFactory.MarkupTagBlock("
      ")))), + new RazorError[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "p"), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 1), + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "strong"), + absoluteIndex: 15, lineIndex: 0, columnIndex: 15, length: 6) + } + }; + yield return new object[] + { + "

      Hello

      World

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", factory.Markup("foo")) + }, + factory.Markup("Hello "), + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("style", factory.Markup("color:red;")) + }, + factory.Markup("World")))), + new RazorError[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "p"), + new SourceLocation(1, 0, 1), + length: 1) + } + }; + } + } + + [Theory] + [MemberData(nameof(IncompleteHelperBlockData))] + public void TagHelperParseTreeRewriter_CreatesErrorForIncompleteTagHelper( + string documentContent, + object expectedOutput, + object expectedErrors) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors, "strong", "p"); + } + + + public static IEnumerable OddlySpacedBlockData + { + get + { + var factory = new SpanFactory(); + + yield return new object[] + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", factory.Markup(" foo")), + new TagHelperAttributeNode( + "style", + new MarkupBlock( + factory.Markup(" color"), + factory.Markup(" :"), + factory.Markup(" red"), + factory.Markup(" ;"), + factory.Markup(" "))) + })) + }; + yield return new object[] + { + "

      Hello World

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", factory.Markup(" foo")), + new TagHelperAttributeNode( + "style", + new MarkupBlock( + factory.Markup(" color"), + factory.Markup(" :"), + factory.Markup(" red"), + factory.Markup(" ;"), + factory.Markup(" "))) + }, + factory.Markup("Hello World"))) + }; + yield return new object[] + { + "

      Hello

      World

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode( + "class", + new MarkupBlock(factory.Markup(" foo"), factory.Markup(" "))) + }, + factory.Markup("Hello")), + factory.Markup(" "), + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode( + "style", + new MarkupBlock(factory.Markup(" color:red;"), factory.Markup(" "))) + }, + factory.Markup("World"))) + }; + } + } + + [Theory] + [MemberData(nameof(OddlySpacedBlockData))] + public void TagHelperParseTreeRewriter_RewritesOddlySpacedTagHelperTagBlocks( + string documentContent, + object expectedOutput) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p"); + } + + public static IEnumerable ComplexAttributeTagHelperBlockData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var dateTimeNowString = "@DateTime.Now"; + 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))))); + var doWhileString = "@do { var foo = bar; Foo foo++; } while (foo);"; + var doWhile = new Func(index => + new MarkupBlock( + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator( + new LocationTagged( + string.Empty, + new SourceLocation(index, 0, index)), + new SourceLocation(index, 0, index)), + new StatementBlock( + factory.CodeTransition(), + factory.Code("do { var foo = bar;").AsStatement(), + new MarkupBlock( + new MarkupTagBlock( + factory.MarkupTransition("")), + factory.Markup("Foo").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + factory.MarkupTransition(""))), + factory + .Code(" foo++; } while (foo);") + .AsStatement() + .Accepts(AcceptedCharacters.None))))); + + var currentFormattedString = "

      "; + yield return new object[] + { + string.Format(currentFormattedString, dateTimeNowString), + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", dateTimeNow(10)), + new TagHelperAttributeNode("style", dateTimeNow(32), HtmlAttributeValueStyle.SingleQuotes) + })) + }; + yield return new object[] + { + string.Format(currentFormattedString, doWhileString), + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", doWhile(10)), + new TagHelperAttributeNode("style", doWhile(83), HtmlAttributeValueStyle.SingleQuotes) + })) + }; + + currentFormattedString = "

      Hello World

      "; + yield return new object[] + { + string.Format(currentFormattedString, dateTimeNowString), + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", dateTimeNow(10)), + new TagHelperAttributeNode("style", dateTimeNow(32), HtmlAttributeValueStyle.SingleQuotes) + }, + factory.Markup("Hello World"))) + }; + yield return new object[] + { + string.Format(currentFormattedString, doWhileString), + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", doWhile(10)), + new TagHelperAttributeNode("style", doWhile(83), HtmlAttributeValueStyle.SingleQuotes) + }, + factory.Markup("Hello World"))) + }; + + currentFormattedString = "

      Hello

      World

      "; + yield return new object[] + { + string.Format(currentFormattedString, dateTimeNowString), + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", dateTimeNow(10)) + }, + factory.Markup("Hello")), + factory.Markup(" "), + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("style", dateTimeNow(45), HtmlAttributeValueStyle.SingleQuotes) + }, + factory.Markup("World"))) + }; + yield return new object[] + { + string.Format(currentFormattedString, doWhileString), + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", doWhile(10)) + }, + factory.Markup("Hello")), + factory.Markup(" "), + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("style", doWhile(96), HtmlAttributeValueStyle.SingleQuotes) + }, + factory.Markup("World"))) + }; + + currentFormattedString = + "

      Hello World inside of strong tag

      "; + yield return new object[] + { + string.Format(currentFormattedString, dateTimeNowString), + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", dateTimeNow(10)), + new TagHelperAttributeNode("style", dateTimeNow(32), HtmlAttributeValueStyle.SingleQuotes) + }, + factory.Markup("Hello World "), + new MarkupTagBlock( + factory.Markup("(" class=\"", 66, 0, 66), + suffix: new LocationTagged("\"", 87, 0, 87)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator( + new LocationTagged(string.Empty, 74, 0, 74), 74, 0, 74), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + factory.Markup("inside of strong tag"), + blockFactory.MarkupTagBlock("
      "))) + }; + } + } + + [Theory] + [MemberData(nameof(ComplexAttributeTagHelperBlockData))] + public void TagHelperParseTreeRewriter_RewritesComplexAttributeTagHelperTagBlocks( + string documentContent, + object expectedOutput) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p"); + } + + public static IEnumerable ComplexTagHelperBlockData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var dateTimeNowString = "@DateTime.Now"; + var dateTimeNow = new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)); + var doWhileString = "@do { var foo = bar;

      Foo

      foo++; } while (foo);"; + var doWhile = new StatementBlock( + factory.CodeTransition(), + factory.Code("do { var foo = bar;").AsStatement(), + new MarkupBlock( + factory.Markup(" "), + new MarkupTagHelperBlock("p", + factory.Markup("Foo")), + factory.Markup(" ").Accepts(AcceptedCharacters.None)), + factory.Code("foo++; } while (foo);") + .AsStatement() + .Accepts(AcceptedCharacters.None)); + + var currentFormattedString = "

      {0}

      "; + yield return new object[] + { + string.Format(currentFormattedString, dateTimeNowString), + new MarkupBlock( + new MarkupTagHelperBlock("p", dateTimeNow)) + }; + yield return new object[] + { + string.Format(currentFormattedString, doWhileString), + new MarkupBlock( + new MarkupTagHelperBlock("p", doWhile)) + }; + + currentFormattedString = "

      Hello World {0}

      "; + yield return new object[] + { + string.Format(currentFormattedString, dateTimeNowString), + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup("Hello World "), + dateTimeNow)) + }; + yield return new object[] + { + string.Format(currentFormattedString, doWhileString), + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup("Hello World "), + doWhile)) + }; + + currentFormattedString = "

      {0}

      {0}

      "; + yield return new object[] + { + string.Format(currentFormattedString, dateTimeNowString), + new MarkupBlock( + new MarkupTagHelperBlock("p", dateTimeNow), + factory.Markup(" "), + new MarkupTagHelperBlock("p", dateTimeNow)) + }; + yield return new object[] + { + string.Format(currentFormattedString, doWhileString), + new MarkupBlock( + new MarkupTagHelperBlock("p", doWhile), + factory.Markup(" "), + new MarkupTagHelperBlock("p", doWhile)) + }; + + currentFormattedString = "

      Hello {0}inside of {0} strong tag

      "; + yield return new object[] + { + string.Format(currentFormattedString, dateTimeNowString), + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup("Hello "), + dateTimeNow, + blockFactory.MarkupTagBlock(""), + factory.Markup("inside of "), + dateTimeNow, + factory.Markup(" strong tag"), + blockFactory.MarkupTagBlock(""))) + }; + yield return new object[] + { + string.Format(currentFormattedString, doWhileString), + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup("Hello "), + doWhile, + blockFactory.MarkupTagBlock(""), + factory.Markup("inside of "), + doWhile, + factory.Markup(" strong tag"), + blockFactory.MarkupTagBlock(""))) + }; + } + } + + [Theory] + [MemberData(nameof(ComplexTagHelperBlockData))] + public void TagHelperParseTreeRewriter_RewritesComplexTagHelperTagBlocks( + string documentContent, + object expectedOutput) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p"); + } + + + public static TheoryData InvalidHtmlBlockData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var dateTimeNow = new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)); + + return new TheoryData + { + { + "<<

      >>

      ", + new MarkupBlock( + blockFactory.MarkupTagBlock("<"), + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("p", + factory.Markup(">>"))) + }, + { + "<

      ", + new MarkupBlock( + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("p", TagMode.SelfClosing)) + }, + { + "< p />", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + new MarkupBlock( + factory.Markup(" p")), + factory.Markup(" />"))) + }, + { + "", + new MarkupBlock( + blockFactory.MarkupTagBlock("", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 1, 0, 1), + suffix: new LocationTagged("\"", 12, 0, 12)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("foo").With(new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 9, 0, 9), + value: new LocationTagged("foo", 9, 0, 9))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(" ")), + new MarkupTagHelperBlock("p", TagMode.SelfClosing)) + }, + { + "/>

      >", + new MarkupBlock( + blockFactory.MarkupTagBlock("")), + factory.Markup(">")) + }, + { + "/>

      >", + new MarkupBlock( + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")), + factory.Markup(">")) + }, + { + "@DateTime.Now/>

      >", + new MarkupBlock( + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")), + factory.Markup(">")) + }, + { + "

      @DateTime.Now / >

      ", + new MarkupBlock( + blockFactory.MarkupTagBlock(""), + new MarkupTagHelperBlock("p", + dateTimeNow, + factory.Markup(" / >"), + blockFactory.MarkupTagBlock("")), + blockFactory.MarkupTagBlock("")) + }, + { + "

      < @DateTime.Now >

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagBlock( + factory.Markup("< "), + dateTimeNow, + factory.Markup(" >")), + blockFactory.MarkupTagBlock(""))) + } + }; + } + } + + [Theory] + [MemberData(nameof(InvalidHtmlBlockData))] + public void TagHelperParseTreeRewriter_AllowsInvalidHtml(string documentContent, object expectedOutput) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p"); + } + + public static TheoryData EmptyAttributeTagHelperData + { + get + { + var factory = new SpanFactory(); + + // documentContent, expectedOutput + return new TheoryData + { + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", new MarkupBlock()) + })) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + new TagHelperAttributeNode("class", new MarkupBlock(), HtmlAttributeValueStyle.SingleQuotes) + })) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List + { + // We expected a markup node here because attribute values without quotes can only ever + // be a single item, hence don't need to be enclosed by a block. + new TagHelperAttributeNode( + "class", + factory.Markup("").With(SpanChunkGenerator.Null), + HtmlAttributeValueStyle.DoubleQuotes), + })) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("class1", new MarkupBlock(), HtmlAttributeValueStyle.SingleQuotes), + new TagHelperAttributeNode( + "class2", + factory.Markup(string.Empty).With(SpanChunkGenerator.Null), + HtmlAttributeValueStyle.DoubleQuotes), + new TagHelperAttributeNode("class3", new MarkupBlock()), + })) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("class1", new MarkupBlock(), HtmlAttributeValueStyle.SingleQuotes), + new TagHelperAttributeNode("class2", new MarkupBlock()), + new TagHelperAttributeNode( + "class3", + factory.Markup(string.Empty).With(SpanChunkGenerator.Null), + HtmlAttributeValueStyle.DoubleQuotes), + })) + }, + }; + } + } + + [Theory] + [MemberData(nameof(EmptyAttributeTagHelperData))] + public void Rewrite_UnderstandsEmptyAttributeTagHelpers(string documentContent, object expectedOutput) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, new RazorError[0], "p"); + } + + public static TheoryData EmptyTagHelperBoundAttributeData + { + get + { + var factory = new SpanFactory(); + var emptyAttributeError = + "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 boolTypeName = typeof(bool).FullName; + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("bound", new MarkupBlock(), HtmlAttributeValueStyle.SingleQuotes) + })), + new[] + { + new RazorError( + string.Format(emptyAttributeError, "bound", "myth", boolTypeName), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 5) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("bound", factory.CodeMarkup(" true"), HtmlAttributeValueStyle.SingleQuotes) + })), + new RazorError[0] + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("bound", factory.CodeMarkup(" "), HtmlAttributeValueStyle.SingleQuotes) + })), + new[] + { + new RazorError( + string.Format(emptyAttributeError, "bound", "myth", boolTypeName), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 5) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("bound", new MarkupBlock(), HtmlAttributeValueStyle.SingleQuotes), + new TagHelperAttributeNode("bound", new MarkupBlock()) + })), + new[] + { + new RazorError( + string.Format(emptyAttributeError, "bound", "myth", boolTypeName), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 5), + new RazorError( + string.Format(emptyAttributeError, "bound", "myth", boolTypeName), + absoluteIndex: 16, lineIndex: 0, columnIndex: 16, length: 5) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("bound", factory.CodeMarkup(" "), HtmlAttributeValueStyle.SingleQuotes), + new TagHelperAttributeNode("bound", factory.CodeMarkup(" ")) + })), + new[] + { + new RazorError( + string.Format(emptyAttributeError, "bound", "myth", boolTypeName), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 5), + new RazorError( + string.Format(emptyAttributeError, "bound", "myth", boolTypeName), + absoluteIndex: 17, lineIndex: 0, columnIndex: 17, length: 5) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("bound", factory.CodeMarkup("true"), HtmlAttributeValueStyle.SingleQuotes), + new TagHelperAttributeNode( + "bound", + factory.CodeMarkup(string.Empty).With(SpanChunkGenerator.Null), + HtmlAttributeValueStyle.DoubleQuotes) + })), + new[] + { + new RazorError( + string.Format(emptyAttributeError, "bound", "myth", boolTypeName), + absoluteIndex: 19, lineIndex: 0, columnIndex: 19, length: 5) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode( + "bound", + factory.CodeMarkup(string.Empty).With(SpanChunkGenerator.Null), + HtmlAttributeValueStyle.DoubleQuotes), + new TagHelperAttributeNode("name", new MarkupBlock(), HtmlAttributeValueStyle.SingleQuotes) + })), + new[] + { + new RazorError( + string.Format(emptyAttributeError, "bound", "myth", boolTypeName), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 5), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode( + "bound", + factory.CodeMarkup(string.Empty).With(SpanChunkGenerator.Null), + HtmlAttributeValueStyle.DoubleQuotes), + new TagHelperAttributeNode("name", factory.Markup(" "), HtmlAttributeValueStyle.SingleQuotes) + })), + new[] + { + new RazorError( + string.Format(emptyAttributeError, "bound", "myth", boolTypeName), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 5), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("bound", factory.CodeMarkup("true"), HtmlAttributeValueStyle.SingleQuotes), + new TagHelperAttributeNode("name", factory.Markup("john"), HtmlAttributeValueStyle.SingleQuotes), + new TagHelperAttributeNode( + "bound", + factory.CodeMarkup(string.Empty).With(SpanChunkGenerator.Null), + HtmlAttributeValueStyle.DoubleQuotes), + new TagHelperAttributeNode( + "name", + factory.Markup(string.Empty).With(SpanChunkGenerator.Null), + HtmlAttributeValueStyle.DoubleQuotes) + })), + new[] + { + new RazorError( + string.Format(emptyAttributeError, "bound", "myth", boolTypeName), + absoluteIndex: 31, lineIndex: 0, columnIndex: 31, length: 5), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("BouND", new MarkupBlock(), HtmlAttributeValueStyle.SingleQuotes) + })), + new[] + { + new RazorError( + string.Format(emptyAttributeError, "BouND", "myth", boolTypeName), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 5), + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("BOUND", new MarkupBlock(), HtmlAttributeValueStyle.SingleQuotes), + new TagHelperAttributeNode("bOUnd", new MarkupBlock()) + })), + new[] + { + new RazorError( + string.Format(emptyAttributeError, "BOUND", "myth", boolTypeName), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 5), + new RazorError( + string.Format(emptyAttributeError, "bOUnd", "myth", boolTypeName), + absoluteIndex: 18, lineIndex: 0, columnIndex: 18, length: 5) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + new List + { + new TagHelperAttributeNode( + "BOUND", + factory.CodeMarkup(string.Empty).With(SpanChunkGenerator.Null), + HtmlAttributeValueStyle.DoubleQuotes), + new TagHelperAttributeNode("nAMe", factory.Markup("john"), HtmlAttributeValueStyle.SingleQuotes) + })), + new[] + { + new RazorError( + string.Format(emptyAttributeError, "BOUND", "myth", boolTypeName), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 5) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + { + new TagHelperAttributeNode( + "bound", + new MarkupBlock( + new MarkupBlock( + factory.CodeMarkup(" "), + new ExpressionBlock( + factory.CSharpCodeMarkup("@"), + factory.CSharpCodeMarkup("true") + .With(new ExpressionChunkGenerator()))), + factory.CodeMarkup(" ")), + HtmlAttributeValueStyle.SingleQuotes) + } + })), + new RazorError[0] + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "myth", + TagMode.SelfClosing, + attributes: new List + { + { + new TagHelperAttributeNode( + "bound", + new MarkupBlock( + new MarkupBlock( + factory.CodeMarkup(" "), + new ExpressionBlock( + factory.CSharpCodeMarkup("@"), + factory.CSharpCodeMarkup("("), + factory.CSharpCodeMarkup("true") + .With(new ExpressionChunkGenerator()), + factory.CSharpCodeMarkup(")"))), + factory.CodeMarkup(" ")), + HtmlAttributeValueStyle.SingleQuotes) + } + })), + new RazorError[0] + }, + }; + } + } + + [Theory] + [MemberData(nameof(EmptyTagHelperBoundAttributeData))] + public void Rewrite_CreatesErrorForEmptyTagHelperBoundAttributes( + string documentContent, + object expectedOutput, + object expectedErrors) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "myth", + TypeName = "mythTagHelper", + AssemblyName = "SomeAssembly", + Attributes = new[] + { + new TagHelperAttributeDescriptor + { + Name = "bound", + PropertyName = "Bound", + TypeName = typeof(bool).FullName + }, + new TagHelperAttributeDescriptor + { + Name = "name", + PropertyName = "Name", + TypeName = typeof(string).FullName, + IsStringProperty = true + } + } + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors); + } + + public static IEnumerable ScriptBlockData + { + get + { + var factory = new SpanFactory(); + + yield return new object[] + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("script", + factory.Markup("", + new MarkupBlock( + new MarkupTagHelperBlock("script", + factory.Markup("Hello World

      "))) + }; + yield return new object[] + { + "

      World

      ", + 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[] + { + " 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 TagHelperAttributeNode("bound-string", null, HtmlAttributeValueStyle.Minimized), + })), + new[] + { + new RazorError( + string.Format(errorFormatNoCloseAngle, "p"), + new SourceLocation(1, 0, 1), + length: 1), + new RazorError( + string.Format(errorFormatUnclosed, "p"), + new SourceLocation(1, 0, 1), + length: 1), + new RazorError( + string.Format(errorFormatNoValue, "bound-string", "p", stringType), 3, 0, 3, 12), + } + }, + { + "

      () + { + new TagHelperAttributeNode("bound-int", null, HtmlAttributeValueStyle.Minimized), + })), + new[] + { + new RazorError( + string.Format(errorFormatNoCloseAngle, "p"), + new SourceLocation(1, 0, 1), + length: 1), + new RazorError( + string.Format(errorFormatUnclosed, "p"), + new SourceLocation(1, 0, 1), + length: 1), + new RazorError(string.Format(errorFormatNoValue, "bound-int", "p", intType), 3, 0, 3, 9), + } + }, + { + "

      () + { + new TagHelperAttributeNode("bound-int", null, HtmlAttributeValueStyle.Minimized), + new TagHelperAttributeNode("bound-string", null, HtmlAttributeValueStyle.Minimized), + })), + new[] + { + new RazorError( + string.Format(errorFormatNoCloseAngle, "p"), + new SourceLocation(1, 0, 1), + length: 1), + new RazorError( + string.Format(errorFormatUnclosed, "p"), + new SourceLocation(1, 0, 1), + length: 1), + new RazorError(string.Format(errorFormatNoValue, "bound-int", "p", intType), 3, 0, 3, 9), + new RazorError( + string.Format(errorFormatNoValue, "bound-string", "p", stringType), 13, 0, 13, 12), + } + }, + { + "() + { + new TagHelperAttributeNode("bound-required-int", null, HtmlAttributeValueStyle.Minimized), + new TagHelperAttributeNode("unbound-required", null, HtmlAttributeValueStyle.Minimized), + new TagHelperAttributeNode("bound-required-string", null, HtmlAttributeValueStyle.Minimized), + }, + children: 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(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), 7, 0, 7, 18), + new RazorError( + string.Format(errorFormatNoValue, "bound-required-string", "input", stringType), + absoluteIndex: 43, + lineIndex: 0, + columnIndex: 43, + length: 21), + new RazorError( + string.Format(errorFormatNoCloseAngle, "p"), + new SourceLocation(65, 0, 65), + length: 1), + new RazorError( + string.Format(errorFormatUnclosed, "p"), + new SourceLocation(65, 0, 65), + length: 1), + new RazorError(string.Format(errorFormatNoValue, "bound-int", "p", intType), 67, 0, 67, 9), + new RazorError( + string.Format(errorFormatNoValue, "bound-string", "p", stringType), + absoluteIndex: 77, + lineIndex: 0, + columnIndex: 77, + length: 12), + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(MinimizedAttributeData_Document))] + [MemberData(nameof(MinimizedAttributeData_CSharpBlock))] + [MemberData(nameof(MinimizedAttributeData_PartialTags))] + public void Rewrite_UnderstandsMinimizedAttributes( + string documentContent, + object expectedOutput, + object expectedErrors) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "input", + TypeName = "InputTagHelper1", + AssemblyName = "SomeAssembly", + Attributes = new[] + { + new TagHelperAttributeDescriptor + { + Name = "bound-required-string", + PropertyName = "BoundRequiredString", + TypeName = typeof(string).FullName, + IsStringProperty = true + } + }, + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "unbound-required" } + } + }, + new TagHelperDescriptor + { + TagName = "input", + TypeName = "InputTagHelper1", + AssemblyName = "SomeAssembly", + Attributes = new[] + { + new TagHelperAttributeDescriptor + { + Name = "bound-required-string", + PropertyName = "BoundRequiredString", + TypeName = typeof(string).FullName, + IsStringProperty = true + } + }, + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "bound-required-string" } + } + }, + new TagHelperDescriptor + { + TagName = "input", + TypeName = "InputTagHelper2", + AssemblyName = "SomeAssembly", + Attributes = new[] + { + new TagHelperAttributeDescriptor + { + Name = "bound-required-int", + PropertyName = "BoundRequiredInt", + TypeName = typeof(int).FullName + } + }, + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "bound-required-int" } + } + }, + new TagHelperDescriptor + { + TagName = "input", + TypeName = "InputTagHelper3", + AssemblyName = "SomeAssembly", + Attributes = new[] + { + new TagHelperAttributeDescriptor + { + Name = "int-dictionary", + PropertyName ="DictionaryOfIntProperty", + TypeName = typeof(IDictionary).FullName + }, + new TagHelperAttributeDescriptor + { + Name = "string-dictionary", + PropertyName = "DictionaryOfStringProperty", + TypeName = typeof(IDictionary).FullName + }, + new TagHelperAttributeDescriptor + { + Name = "int-prefix-", + PropertyName = "DictionaryOfIntProperty", + TypeName = typeof(int).FullName, + IsIndexer = true + }, + new TagHelperAttributeDescriptor + { + Name = "string-prefix-", + PropertyName = "DictionaryOfStringProperty", + TypeName = typeof(string).FullName, + IsIndexer = true, + IsStringProperty = true + } + } + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper", + AssemblyName = "SomeAssembly", + Attributes = new[] + { + new TagHelperAttributeDescriptor + { + Name = "bound-string", + PropertyName = "BoundRequiredString", + TypeName = typeof(string).FullName, + IsStringProperty = true + }, + new TagHelperAttributeDescriptor + { + Name = "bound-int", + PropertyName = "BoundRequiredString", + TypeName = typeof(int).FullName + } + } + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperBlockTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperBlockTest.cs new file mode 100644 index 0000000000..6801d7aa15 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperBlockTest.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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + public class TagHelperBlockTest + { + [Fact] + public void FlattenFlattensSelfClosingTagHelpers() + { + // Arrange + var spanFactory = new SpanFactory(); + var blockFactory = new BlockFactory(spanFactory); + var tagHelper = (TagHelperBlock)blockFactory.TagHelperBlock( + tagName: "input", + tagMode: TagMode.SelfClosing, + start: SourceLocation.Zero, + startTag: blockFactory.MarkupTagBlock(""), + children: new SyntaxTreeNode[0], + endTag: null); + spanFactory.Reset(); + var expectedNode = spanFactory.Markup(""); + + // Act + var flattenedNodes = tagHelper.Flatten(); + + // Assert + var node = Assert.Single(flattenedNodes); + Assert.True(node.EquivalentTo(expectedNode)); + } + + [Fact] + public void FlattenFlattensStartAndEndTagTagHelpers() + { + // Arrange + var spanFactory = new SpanFactory(); + var blockFactory = new BlockFactory(spanFactory); + var tagHelper = (TagHelperBlock)blockFactory.TagHelperBlock( + tagName: "div", + tagMode: TagMode.StartTagAndEndTag, + start: SourceLocation.Zero, + startTag: blockFactory.MarkupTagBlock("

      "), + children: new SyntaxTreeNode[0], + endTag: blockFactory.MarkupTagBlock("
      ")); + spanFactory.Reset(); + var expectedStartTag = spanFactory.Markup("
      "); + var expectedEndTag = spanFactory.Markup("
      "); + + // Act + var flattenedNodes = tagHelper.Flatten(); + + // Assert + Assert.Collection( + flattenedNodes, + first => + { + Assert.True(first.EquivalentTo(expectedStartTag)); + }, + second => + { + Assert.True(second.EquivalentTo(expectedEndTag)); + }); + } + + [Fact] + public void FlattenFlattensStartAndEndTagWithChildrenTagHelpers() + { + // Arrange + var spanFactory = new SpanFactory(); + var blockFactory = new BlockFactory(spanFactory); + var tagHelper = (TagHelperBlock)blockFactory.TagHelperBlock( + tagName: "div", + tagMode: TagMode.StartTagAndEndTag, + start: SourceLocation.Zero, + startTag: blockFactory.MarkupTagBlock("
      "), + children: new SyntaxTreeNode[] { spanFactory.Markup("Hello World") }, + endTag: blockFactory.MarkupTagBlock("
      ")); + spanFactory.Reset(); + var expectedStartTag = spanFactory.Markup("
      "); + var expectedChildren = spanFactory.Markup("Hello World"); + var expectedEndTag = spanFactory.Markup("
      "); + + // Act + var flattenedNodes = tagHelper.Flatten(); + + // Assert + Assert.Collection( + flattenedNodes, + first => + { + Assert.True(first.EquivalentTo(expectedStartTag)); + }, + second => + { + Assert.True(second.EquivalentTo(expectedChildren)); + }, + third => + { + Assert.True(third.EquivalentTo(expectedEndTag)); + }); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDescriptorProviderTest.cs new file mode 100644 index 0000000000..a8ebd5f399 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDescriptorProviderTest.cs @@ -0,0 +1,498 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + public class TagHelperDescriptorProviderTest + { + public static TheoryData RequiredParentData + { + get + { + var strongPParent = new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly", + RequiredParent = "p", + }; + var strongDivParent = new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly", + RequiredParent = "div", + }; + var catchAllPParent = new TagHelperDescriptor + { + TagName = "*", + TypeName = "CatchAllTagHelper", + AssemblyName = "SomeAssembly", + RequiredParent = "p", + }; + + return new TheoryData< + string, // tagName + string, // parentTagName + IEnumerable, // availableDescriptors + IEnumerable> // expectedDescriptors + { + { + "strong", + "p", + new[] { strongPParent, strongDivParent }, + new[] { strongPParent } + }, + { + "strong", + "div", + new[] { strongPParent, strongDivParent, catchAllPParent }, + new[] { strongDivParent } + }, + { + "strong", + "p", + new[] { strongPParent, strongDivParent, catchAllPParent }, + new[] { strongPParent, catchAllPParent } + }, + { + "custom", + "p", + new[] { strongPParent, strongDivParent, catchAllPParent }, + new[] { catchAllPParent } + }, + }; + } + } + + [Theory] + [MemberData(nameof(RequiredParentData))] + public void GetDescriptors_ReturnsDescriptorsParentTags( + string tagName, + string parentTagName, + object availableDescriptors, + object expectedDescriptors) + { + // Arrange + var provider = new TagHelperDescriptorProvider((IEnumerable)availableDescriptors); + + // Act + var resolvedDescriptors = provider.GetDescriptors( + tagName, + attributes: Enumerable.Empty>(), + parentTagName: parentTagName); + + // Assert + Assert.Equal((IEnumerable)expectedDescriptors, resolvedDescriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + public static TheoryData RequiredAttributeData + { + get + { + var divDescriptor = new TagHelperDescriptor + { + TagName = "div", + TypeName = "DivTagHelper", + AssemblyName = "SomeAssembly", + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "style" } } + }; + var inputDescriptor = new TagHelperDescriptor + { + TagName = "input", + TypeName = "InputTagHelper", + AssemblyName = "SomeAssembly", + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" }, + new TagHelperRequiredAttributeDescriptor { Name = "style" } + } + }; + var inputWildcardPrefixDescriptor = new TagHelperDescriptor + { + TagName = "input", + TypeName = "InputWildCardAttribute", + AssemblyName = "SomeAssembly", + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor + { + Name = "nodashprefix", + NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch, + } + } + }; + var catchAllDescriptor = new TagHelperDescriptor + { + TagName = TagHelperDescriptorProvider.ElementCatchAllTarget, + TypeName = "CatchAllTagHelper", + AssemblyName = "SomeAssembly", + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } } + }; + var catchAllDescriptor2 = new TagHelperDescriptor + { + TagName = TagHelperDescriptorProvider.ElementCatchAllTarget, + TypeName = "CatchAllTagHelper", + AssemblyName = "SomeAssembly", + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "custom" }, + new TagHelperRequiredAttributeDescriptor { Name = "class" } + } + }; + var catchAllWildcardPrefixDescriptor = new TagHelperDescriptor + { + TagName = TagHelperDescriptorProvider.ElementCatchAllTarget, + TypeName = "CatchAllWildCardAttribute", + AssemblyName = "SomeAssembly", + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor + { + Name = "prefix-", + NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch, + } + } + }; + var defaultAvailableDescriptors = + new[] { divDescriptor, inputDescriptor, catchAllDescriptor, catchAllDescriptor2 }; + var defaultWildcardDescriptors = + new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor }; + Func> kvp = + (name) => new KeyValuePair(name, "test value"); + + return new TheoryData< + string, // tagName + IEnumerable>, // providedAttributes + IEnumerable, // availableDescriptors + IEnumerable> // expectedDescriptors + { + { + "div", + new[] { kvp("custom") }, + defaultAvailableDescriptors, + Enumerable.Empty() + }, + { "div", new[] { kvp("style") }, defaultAvailableDescriptors, new[] { divDescriptor } }, + { "div", new[] { kvp("class") }, defaultAvailableDescriptors, new[] { catchAllDescriptor } }, + { + "div", + new[] { kvp("class"), kvp("style") }, + defaultAvailableDescriptors, + new[] { divDescriptor, catchAllDescriptor } + }, + { + "div", + new[] { kvp("class"), kvp("style"), kvp("custom") }, + defaultAvailableDescriptors, + new[] { divDescriptor, catchAllDescriptor, catchAllDescriptor2 } + }, + { + "input", + new[] { kvp("class"), kvp("style") }, + defaultAvailableDescriptors, + new[] { inputDescriptor, catchAllDescriptor } + }, + { + "input", + new[] { kvp("nodashprefixA") }, + defaultWildcardDescriptors, + new[] { inputWildcardPrefixDescriptor } + }, + { + "input", + new[] { kvp("nodashprefix-ABC-DEF"), kvp("random") }, + defaultWildcardDescriptors, + new[] { inputWildcardPrefixDescriptor } + }, + { + "input", + new[] { kvp("prefixABCnodashprefix") }, + defaultWildcardDescriptors, + Enumerable.Empty() + }, + { + "input", + new[] { kvp("prefix-") }, + defaultWildcardDescriptors, + Enumerable.Empty() + }, + { + "input", + new[] { kvp("nodashprefix") }, + defaultWildcardDescriptors, + Enumerable.Empty() + }, + { + "input", + new[] { kvp("prefix-A") }, + defaultWildcardDescriptors, + new[] { catchAllWildcardPrefixDescriptor } + }, + { + "input", + new[] { kvp("prefix-ABC-DEF"), kvp("random") }, + defaultWildcardDescriptors, + new[] { catchAllWildcardPrefixDescriptor } + }, + { + "input", + new[] { kvp("prefix-abc"), kvp("nodashprefix-def") }, + defaultWildcardDescriptors, + new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor } + }, + { + "input", + new[] { kvp("class"), kvp("prefix-abc"), kvp("onclick"), kvp("nodashprefix-def"), kvp("style") }, + defaultWildcardDescriptors, + new[] { inputWildcardPrefixDescriptor, catchAllWildcardPrefixDescriptor } + }, + }; + } + } + + [Theory] + [MemberData(nameof(RequiredAttributeData))] + public void GetDescriptors_ReturnsDescriptorsWithRequiredAttributes( + string tagName, + IEnumerable> providedAttributes, + object availableDescriptors, + object expectedDescriptors) + { + // Arrange + var provider = new TagHelperDescriptorProvider((IEnumerable)availableDescriptors); + + // Act + var resolvedDescriptors = provider.GetDescriptors(tagName, providedAttributes, parentTagName: "p"); + + // Assert + Assert.Equal((IEnumerable)expectedDescriptors, resolvedDescriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void GetDescriptors_ReturnsEmptyDescriptorsWithPrefixAsTagName() + { + // Arrange + var catchAllDescriptor = CreatePrefixedDescriptor( + "th", + TagHelperDescriptorProvider.ElementCatchAllTarget, + "foo1"); + var descriptors = new[] { catchAllDescriptor }; + var provider = new TagHelperDescriptorProvider(descriptors); + + // Act + var resolvedDescriptors = provider.GetDescriptors( + tagName: "th", + attributes: Enumerable.Empty>(), + parentTagName: "p"); + + // Assert + Assert.Empty(resolvedDescriptors); + } + + [Fact] + public void GetDescriptors_OnlyUnderstandsSinglePrefix() + { + // Arrange + var divDescriptor = CreatePrefixedDescriptor("th:", "div", "foo1"); + var spanDescriptor = CreatePrefixedDescriptor("th2:", "span", "foo2"); + var descriptors = new[] { divDescriptor, spanDescriptor }; + var provider = new TagHelperDescriptorProvider(descriptors); + + // Act + var retrievedDescriptorsDiv = provider.GetDescriptors( + tagName: "th:div", + attributes: Enumerable.Empty>(), + parentTagName: "p"); + var retrievedDescriptorsSpan = provider.GetDescriptors( + tagName: "th2:span", + attributes: Enumerable.Empty>(), + parentTagName: "p"); + + // Assert + var descriptor = Assert.Single(retrievedDescriptorsDiv); + Assert.Same(divDescriptor, descriptor); + Assert.Empty(retrievedDescriptorsSpan); + } + + [Fact] + public void GetDescriptors_ReturnsCatchAllDescriptorsForPrefixedTags() + { + // Arrange + var catchAllDescriptor = CreatePrefixedDescriptor("th:", TagHelperDescriptorProvider.ElementCatchAllTarget, "foo1"); + var descriptors = new[] { catchAllDescriptor }; + var provider = new TagHelperDescriptorProvider(descriptors); + + // Act + var retrievedDescriptorsDiv = provider.GetDescriptors( + tagName: "th:div", + attributes: Enumerable.Empty>(), + parentTagName: "p"); + var retrievedDescriptorsSpan = provider.GetDescriptors( + tagName: "th:span", + attributes: Enumerable.Empty>(), + parentTagName: "p"); + + // Assert + var descriptor = Assert.Single(retrievedDescriptorsDiv); + Assert.Same(catchAllDescriptor, descriptor); + descriptor = Assert.Single(retrievedDescriptorsSpan); + Assert.Same(catchAllDescriptor, descriptor); + } + + [Fact] + public void GetDescriptors_ReturnsDescriptorsForPrefixedTags() + { + // Arrange + var divDescriptor = CreatePrefixedDescriptor("th:", "div", "foo1"); + var descriptors = new[] { divDescriptor }; + var provider = new TagHelperDescriptorProvider(descriptors); + + // Act + var retrievedDescriptors = provider.GetDescriptors( + tagName: "th:div", + attributes: Enumerable.Empty>(), + parentTagName: "p"); + + // Assert + var descriptor = Assert.Single(retrievedDescriptors); + Assert.Same(divDescriptor, descriptor); + } + + [Theory] + [InlineData("*")] + [InlineData("div")] + public void GetDescriptors_ReturnsNothingForUnprefixedTags(string tagName) + { + // Arrange + var divDescriptor = CreatePrefixedDescriptor("th:", tagName, "foo1"); + var descriptors = new[] { divDescriptor }; + var provider = new TagHelperDescriptorProvider(descriptors); + + // Act + var retrievedDescriptorsDiv = provider.GetDescriptors( + tagName: "div", + attributes: Enumerable.Empty>(), + parentTagName: "p"); + + // Assert + Assert.Empty(retrievedDescriptorsDiv); + } + + [Fact] + public void GetDescriptors_ReturnsNothingForUnregisteredTags() + { + // Arrange + var divDescriptor = new TagHelperDescriptor + { + TagName = "div", + TypeName = "foo1", + AssemblyName = "SomeAssembly", + }; + var spanDescriptor = new TagHelperDescriptor + { + TagName = "span", + TypeName = "foo2", + AssemblyName = "SomeAssembly", + }; + var descriptors = new TagHelperDescriptor[] { divDescriptor, spanDescriptor }; + var provider = new TagHelperDescriptorProvider(descriptors); + + // Act + var retrievedDescriptors = provider.GetDescriptors( + tagName: "foo", + attributes: Enumerable.Empty>(), + parentTagName: "p"); + + // Assert + Assert.Empty(retrievedDescriptors); + } + + [Fact] + public void GetDescriptors_ReturnsCatchAllsWithEveryTagName() + { + // Arrange + var divDescriptor = new TagHelperDescriptor + { + TagName = "div", + TypeName = "foo1", + AssemblyName = "SomeAssembly", + }; + var spanDescriptor = new TagHelperDescriptor + { + TagName = "span", + TypeName = "foo2", + AssemblyName = "SomeAssembly", + }; + var catchAllDescriptor = new TagHelperDescriptor + { + TagName = TagHelperDescriptorProvider.ElementCatchAllTarget, + TypeName = "foo3", + AssemblyName = "SomeAssembly", + }; + var descriptors = new TagHelperDescriptor[] { divDescriptor, spanDescriptor, catchAllDescriptor }; + var provider = new TagHelperDescriptorProvider(descriptors); + + // Act + var divDescriptors = provider.GetDescriptors( + tagName: "div", + attributes: Enumerable.Empty>(), + parentTagName: "p"); + var spanDescriptors = provider.GetDescriptors( + tagName: "span", + attributes: Enumerable.Empty>(), + parentTagName: "p"); + + // Assert + // For divs + Assert.Equal(2, divDescriptors.Count()); + Assert.Contains(divDescriptor, divDescriptors); + Assert.Contains(catchAllDescriptor, divDescriptors); + + // For spans + Assert.Equal(2, spanDescriptors.Count()); + Assert.Contains(spanDescriptor, spanDescriptors); + Assert.Contains(catchAllDescriptor, spanDescriptors); + } + + [Fact] + public void GetDescriptors_DuplicateDescriptorsAreNotPartOfTagHelperDescriptorPool() + { + // Arrange + var divDescriptor = new TagHelperDescriptor + { + TagName = "div", + TypeName = "foo1", + AssemblyName = "SomeAssembly", + }; + var descriptors = new TagHelperDescriptor[] { divDescriptor, divDescriptor }; + var provider = new TagHelperDescriptorProvider(descriptors); + + // Act + var retrievedDescriptors = provider.GetDescriptors( + tagName: "div", + attributes: Enumerable.Empty>(), + parentTagName: "p"); + + // Assert + var descriptor = Assert.Single(retrievedDescriptors); + Assert.Same(divDescriptor, descriptor); + } + + private static TagHelperDescriptor CreatePrefixedDescriptor(string prefix, string tagName, string typeName) + { + return new TagHelperDescriptor + { + Prefix = prefix, + TagName = tagName, + TypeName = typeName, + AssemblyName = "SomeAssembly" + }; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDescriptorTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDescriptorTest.cs new file mode 100644 index 0000000000..5a94ff7bce --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDescriptorTest.cs @@ -0,0 +1,556 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + public class TagHelperDescriptorTest + { + [Fact] + public void Constructor_CorrectlyCreatesCopy() + { + // Arrange + var descriptor = new TagHelperDescriptor + { + Prefix = "prefix", + TagName = "tag-name", + TypeName = "TypeName", + AssemblyName = "AsssemblyName", + Attributes = new List + { + new TagHelperAttributeDescriptor + { + Name = "test-attribute", + PropertyName = "TestAttribute", + TypeName = "string" + } + }, + RequiredAttributes = new List + { + new TagHelperRequiredAttributeDescriptor + { + Name = "test-required-attribute" + } + }, + AllowedChildren = new[] { "child" }, + RequiredParent = "required parent", + TagStructure = TagStructure.NormalOrSelfClosing, + DesignTimeDescriptor = new TagHelperDesignTimeDescriptor() + }; + + descriptor.PropertyBag.Add("foo", "bar"); + + // Act + var copyDescriptor = new TagHelperDescriptor(descriptor); + + // Assert + Assert.Equal(descriptor, copyDescriptor, CaseSensitiveTagHelperDescriptorComparer.Default); + Assert.Same(descriptor.Attributes, copyDescriptor.Attributes); + Assert.Same(descriptor.RequiredAttributes, copyDescriptor.RequiredAttributes); + } + + [Fact] + public void TagHelperDescriptor_CanBeSerialized() + { + // Arrange + var descriptor = new TagHelperDescriptor + { + Prefix = "prefix:", + TagName = "tag name", + TypeName = "type name", + AssemblyName = "assembly name", + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor + { + Name = "required attribute one", + NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch + }, + new TagHelperRequiredAttributeDescriptor + { + Name = "required attribute two", + NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch, + Value = "something", + ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch, + } + }, + AllowedChildren = new[] { "allowed child one" }, + RequiredParent = "parent name", + DesignTimeDescriptor = new TagHelperDesignTimeDescriptor + { + Summary = "usage summary", + Remarks = "usage remarks", + OutputElementHint = "some-tag" + }, + }; + + var expectedSerializedDescriptor = + $"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," + + $"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," + + $"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," + + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" + + $"[{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute one\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":1," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":null," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":0}}," + + $"{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute two\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":0," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":\"something\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":2}}]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\"]," + + $"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," + + $"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," + + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":{{" + + $"\"{ nameof(TagHelperDesignTimeDescriptor.Summary) }\":\"usage summary\"," + + $"\"{ nameof(TagHelperDesignTimeDescriptor.Remarks) }\":\"usage remarks\"," + + $"\"{ nameof(TagHelperDesignTimeDescriptor.OutputElementHint) }\":\"some-tag\"}}," + + $"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":{{}}}}"; + + // Act + var serializedDescriptor = JsonConvert.SerializeObject(descriptor); + + // Assert + Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal); + } + + [Fact] + public void TagHelperDescriptor_WithAttributes_CanBeSerialized() + { + // Arrange + var descriptor = new TagHelperDescriptor + { + Prefix = "prefix:", + TagName = "tag name", + TypeName = "type name", + AssemblyName = "assembly name", + Attributes = new[] + { + new TagHelperAttributeDescriptor + { + Name = "attribute one", + PropertyName = "property name", + TypeName = "property type name", + IsEnum = true, + }, + new TagHelperAttributeDescriptor + { + Name = "attribute two", + PropertyName = "property name", + TypeName = typeof(string).FullName, + IsStringProperty = true + }, + }, + TagStructure = TagStructure.NormalOrSelfClosing + }; + + var expectedSerializedDescriptor = + $"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," + + $"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," + + $"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," + + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," + + $"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":null," + + $"\"{ nameof(TagHelperDescriptor.TagStructure) }\":1," + + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null," + + $"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":{{}}}}"; + + // Act + var serializedDescriptor = JsonConvert.SerializeObject(descriptor); + + // Assert + Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal); + } + + [Fact] + public void TagHelperDescriptor_WithIndexerAttributes_CanBeSerialized() + { + // Arrange + var descriptor = new TagHelperDescriptor + { + Prefix = "prefix:", + TagName = "tag name", + TypeName = "type name", + AssemblyName = "assembly name", + Attributes = new[] + { + new TagHelperAttributeDescriptor + { + Name = "attribute one", + PropertyName = "property name", + TypeName = "property type name", + IsIndexer = true, + IsEnum = true, + }, + new TagHelperAttributeDescriptor + { + Name = "attribute two", + PropertyName = "property name", + TypeName = typeof(string).FullName, + IsIndexer = true, + IsEnum = false, + IsStringProperty = true + }, + }, + AllowedChildren = new[] { "allowed child one", "allowed child two" }, + RequiredParent = "parent name" + }; + + var expectedSerializedDescriptor = + $"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," + + $"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," + + $"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," + + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," + + $"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," + + $"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," + + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null," + + $"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":{{}}}}"; + + // Act + var serializedDescriptor = JsonConvert.SerializeObject(descriptor); + + // Assert + Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal); + } + + [Fact] + public void TagHelperDescriptor_WithPropertyBagElements_CanBeSerialized() + { + // Arrange + var descriptor = new TagHelperDescriptor + { + Prefix = "prefix:", + TagName = "tag name", + TypeName = "type name", + AssemblyName = "assembly name" + }; + + descriptor.PropertyBag.Add("key one", "value one"); + descriptor.PropertyBag.Add("key two", "value two"); + + var expectedSerializedDescriptor = + $"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," + + $"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," + + $"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," + + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," + + $"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":null," + + $"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," + + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null," + + $"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":" + + "{\"key one\":\"value one\",\"key two\":\"value two\"}}"; + + // Act + var serializedDescriptor = JsonConvert.SerializeObject(descriptor); + + // Assert + Assert.Equal(expectedSerializedDescriptor, serializedDescriptor); + } + + [Fact] + public void TagHelperDescriptor_CanBeDeserialized() + { + // Arrange + var serializedDescriptor = + $"{{\"{nameof(TagHelperDescriptor.Prefix)}\":\"prefix:\"," + + $"\"{nameof(TagHelperDescriptor.TagName)}\":\"tag name\"," + + $"\"{nameof(TagHelperDescriptor.FullTagName)}\":\"prefix:tag name\"," + + $"\"{nameof(TagHelperDescriptor.TypeName)}\":\"type name\"," + + $"\"{nameof(TagHelperDescriptor.AssemblyName)}\":\"assembly name\"," + + $"\"{nameof(TagHelperDescriptor.Attributes)}\":[]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" + + $"[{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute one\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":1," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":null," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":0}}," + + $"{{\"{ nameof(TagHelperRequiredAttributeDescriptor.Name)}\":\"required attribute two\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.NameComparison) }\":0," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.Value) }\":\"something\"," + + $"\"{ nameof(TagHelperRequiredAttributeDescriptor.ValueComparison) }\":2}}]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," + + $"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":\"parent name\"," + + $"\"{nameof(TagHelperDescriptor.TagStructure)}\":2," + + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":{{" + + $"\"{ nameof(TagHelperDesignTimeDescriptor.Summary) }\":\"usage summary\"," + + $"\"{ nameof(TagHelperDesignTimeDescriptor.Remarks) }\":\"usage remarks\"," + + $"\"{ nameof(TagHelperDesignTimeDescriptor.OutputElementHint) }\":\"some-tag\"}}}}"; + var expectedDescriptor = new TagHelperDescriptor + { + Prefix = "prefix:", + TagName = "tag name", + TypeName = "type name", + AssemblyName = "assembly name", + RequiredAttributes = new[] + { + new TagHelperRequiredAttributeDescriptor + { + Name = "required attribute one", + NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch + }, + new TagHelperRequiredAttributeDescriptor + { + Name = "required attribute two", + NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch, + Value = "something", + ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch, + } + }, + AllowedChildren = new[] { "allowed child one", "allowed child two" }, + RequiredParent = "parent name", + DesignTimeDescriptor = new TagHelperDesignTimeDescriptor + { + Summary = "usage summary", + Remarks = "usage remarks", + OutputElementHint = "some-tag" + } + }; + + // Act + var descriptor = JsonConvert.DeserializeObject(serializedDescriptor); + + // Assert + Assert.NotNull(descriptor); + Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal); + Assert.Empty(descriptor.Attributes); + Assert.Equal(expectedDescriptor.RequiredAttributes, descriptor.RequiredAttributes, TagHelperRequiredAttributeDescriptorComparer.Default); + Assert.Equal( + expectedDescriptor.DesignTimeDescriptor, + descriptor.DesignTimeDescriptor, + TagHelperDesignTimeDescriptorComparer.Default); + Assert.Empty(descriptor.PropertyBag); + } + + [Fact] + public void TagHelperDescriptor_WithAttributes_CanBeDeserialized() + { + // Arrange + var serializedDescriptor = + $"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," + + $"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," + + $"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," + + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," + + $"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":null," + + $"\"{nameof(TagHelperDescriptor.TagStructure)}\":0," + + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}"; + var expectedDescriptor = new TagHelperDescriptor + { + Prefix = "prefix:", + TagName = "tag name", + TypeName = "type name", + AssemblyName = "assembly name", + Attributes = new[] + { + new TagHelperAttributeDescriptor + { + Name = "attribute one", + PropertyName = "property name", + TypeName = "property type name", + IsEnum = true, + }, + new TagHelperAttributeDescriptor + { + Name = "attribute two", + PropertyName = "property name", + TypeName = typeof(string).FullName, + IsEnum = false, + IsStringProperty = true + }, + }, + AllowedChildren = new[] { "allowed child one", "allowed child two" } + }; + + // Act + var descriptor = JsonConvert.DeserializeObject(serializedDescriptor); + + // Assert + Assert.NotNull(descriptor); + Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.Attributes, descriptor.Attributes, TagHelperAttributeDescriptorComparer.Default); + Assert.Empty(descriptor.RequiredAttributes); + Assert.Empty(descriptor.PropertyBag); + } + + [Fact] + public void TagHelperDescriptor_WithIndexerAttributes_CanBeDeserialized() + { + // Arrange + var serializedDescriptor = + $"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," + + $"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," + + $"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," + + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsIndexer) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsEnum) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," + + $"\"{ nameof(TagHelperDescriptor.RequiredParent) }\":null," + + $"\"{nameof(TagHelperDescriptor.TagStructure)}\":1," + + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null," + + $"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":{{}}}}"; + + var expectedDescriptor = new TagHelperDescriptor + { + Prefix = "prefix:", + TagName = "tag name", + TypeName = "type name", + AssemblyName = "assembly name", + Attributes = new[] + { + new TagHelperAttributeDescriptor + { + Name = "attribute one", + PropertyName = "property name", + TypeName = "property type name", + IsIndexer = true, + IsEnum = true, + }, + new TagHelperAttributeDescriptor + { + Name = "attribute two", + PropertyName = "property name", + TypeName = typeof(string).FullName, + IsIndexer = true, + IsEnum = false, + IsStringProperty = true + } + }, + TagStructure = TagStructure.NormalOrSelfClosing + }; + + // Act + var descriptor = JsonConvert.DeserializeObject(serializedDescriptor); + + // Assert + Assert.NotNull(descriptor); + Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.Attributes, descriptor.Attributes, TagHelperAttributeDescriptorComparer.Default); + Assert.Empty(descriptor.RequiredAttributes); + Assert.Empty(descriptor.PropertyBag); + } + + [Fact] + public void TagHelperDescriptor_WithPropertyBagElements_CanBeDeserialized() + { + // Arrange + var serializedDescriptor = + $"{{\"{nameof(TagHelperDescriptor.Prefix)}\":\"prefix:\"," + + $"\"{nameof(TagHelperDescriptor.TagName)}\":\"tag name\"," + + $"\"{nameof(TagHelperDescriptor.TypeName)}\":\"type name\"," + + $"\"{nameof(TagHelperDescriptor.AssemblyName)}\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.PropertyBag) }\":" + + "{\"key one\":\"value one\",\"key two\":\"value two\"}}"; + var expectedDescriptor = new TagHelperDescriptor + { + Prefix = "prefix:", + TagName = "tag name", + TypeName = "type name", + AssemblyName = "assembly name" + }; + + expectedDescriptor.PropertyBag.Add("key one", "value one"); + expectedDescriptor.PropertyBag.Add("key two", "value two"); + + // Act + var descriptor = JsonConvert.DeserializeObject(serializedDescriptor); + + // Assert + Assert.NotNull(descriptor); + Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal); + Assert.Empty(descriptor.Attributes); + Assert.Empty(descriptor.RequiredAttributes); + Assert.Equal(expectedDescriptor.PropertyBag["key one"], descriptor.PropertyBag["key one"]); + Assert.Equal(expectedDescriptor.PropertyBag["key two"], descriptor.PropertyBag["key two"]); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDesignTimeDescriptorComparer.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDesignTimeDescriptorComparer.cs new file mode 100644 index 0000000000..6d45cb8b50 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDesignTimeDescriptorComparer.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 TagHelperDesignTimeDescriptorComparer : IEqualityComparer + { + public static readonly TagHelperDesignTimeDescriptorComparer Default = + new TagHelperDesignTimeDescriptorComparer(); + + private TagHelperDesignTimeDescriptorComparer() + { + } + + public bool Equals(TagHelperDesignTimeDescriptor descriptorX, TagHelperDesignTimeDescriptor descriptorY) + { + if (descriptorX == descriptorY) + { + return true; + } + + Assert.NotNull(descriptorX); + Assert.NotNull(descriptorY); + Assert.Equal(descriptorX.Summary, descriptorY.Summary, StringComparer.Ordinal); + Assert.Equal(descriptorX.Remarks, descriptorY.Remarks, StringComparer.Ordinal); + Assert.Equal(descriptorX.OutputElementHint, descriptorY.OutputElementHint, StringComparer.Ordinal); + + return true; + } + + public int GetHashCode(TagHelperDesignTimeDescriptor descriptor) + { + var hashCodeCombiner = HashCodeCombiner.Start(); + + hashCodeCombiner.Add(descriptor.Summary, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.Remarks, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.OutputElementHint, StringComparer.Ordinal); + + return hashCodeCombiner; + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDirectiveSpanVisitorTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDirectiveSpanVisitorTest.cs new file mode 100644 index 0000000000..2a6e007843 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperDirectiveSpanVisitorTest.cs @@ -0,0 +1,423 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Internal; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + public class TagHelperDirectiveSpanVisitorTest + { + public static TheoryData QuotedTagHelperDirectivesData + { + get + { + var factory = new SpanFactory(); + + // document, expectedDescriptors + return new TheoryData> + { + { + new MarkupBlock(factory.Code("\"*, someAssembly\"").AsAddTagHelper("*, someAssembly")), + new[] + { + new TagHelperDirectiveDescriptor + { + DirectiveText = "*, someAssembly", + DirectiveType = TagHelperDirectiveType.AddTagHelper + }, + } + }, + { + new MarkupBlock(factory.Code("\"*, someAssembly\"").AsRemoveTagHelper("*, someAssembly")), + new[] + { + new TagHelperDirectiveDescriptor + { + DirectiveText = "*, someAssembly", + DirectiveType = TagHelperDirectiveType.RemoveTagHelper + }, + } + }, + { + new MarkupBlock(factory.Code("\"th:\"").AsTagHelperPrefixDirective("th:")), + new[] + { + new TagHelperDirectiveDescriptor + { + DirectiveText = "th:", + DirectiveType = TagHelperDirectiveType.TagHelperPrefix + }, + } + }, + { + new MarkupBlock(factory.Code(" \"*, someAssembly \" ").AsAddTagHelper("*, someAssembly ")), + new[] + { + new TagHelperDirectiveDescriptor + { + DirectiveText = "*, someAssembly", + DirectiveType = TagHelperDirectiveType.AddTagHelper + }, + } + }, + { + new MarkupBlock(factory.Code(" \"*, someAssembly \" ").AsRemoveTagHelper("*, someAssembly ")), + new[] + { + new TagHelperDirectiveDescriptor + { + DirectiveText = "*, someAssembly", + DirectiveType = TagHelperDirectiveType.RemoveTagHelper + }, + } + }, + { + new MarkupBlock(factory.Code(" \" th :\"").AsTagHelperPrefixDirective(" th :")), + new[] + { + new TagHelperDirectiveDescriptor + { + DirectiveText = "th :", + DirectiveType = TagHelperDirectiveType.TagHelperPrefix + }, + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(QuotedTagHelperDirectivesData))] + public void GetDescriptors_LocatesQuotedTagHelperDirectives_CreatesDirectiveDescriptors( + object document, + object expectedDescriptors) + { + // Arrange + var resolver = new TestTagHelperDescriptorResolver(); + var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor(resolver, new ErrorSink()); + + // Act + tagHelperDirectiveSpanVisitor.GetDescriptors((MarkupBlock)document); + + // Assert + Assert.Equal( + (IEnumerable)expectedDescriptors, + resolver.DirectiveDescriptors, + TagHelperDirectiveDescriptorComparer.Default); + } + + [Fact] + public void GetDescriptors_InvokesResolveOnceForAllDirectives() + { + // Arrange + var factory = new SpanFactory(); + var resolver = new Mock(); + resolver.Setup(mock => mock.Resolve(It.IsAny())) + .Returns(Enumerable.Empty()); + var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor( + resolver.Object, + new ErrorSink()); + var document = new MarkupBlock( + factory.Code("one").AsAddTagHelper("one"), + factory.Code("two").AsRemoveTagHelper("two"), + factory.Code("three").AsRemoveTagHelper("three"), + factory.Code("four").AsTagHelperPrefixDirective("four")); + + // Act + tagHelperDirectiveSpanVisitor.GetDescriptors(document); + + // Assert + resolver.Verify(mock => mock.Resolve(It.IsAny()), Times.Once); + } + + [Fact] + public void GetDescriptors_LocatesTagHelperChunkGenerator_CreatesDirectiveDescriptors() + { + // Arrange + var factory = new SpanFactory(); + var resolver = new TestTagHelperDescriptorResolver(); + var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor(resolver, new ErrorSink()); + var document = new MarkupBlock( + factory.Code("one").AsAddTagHelper("one"), + factory.Code("two").AsRemoveTagHelper("two"), + factory.Code("three").AsRemoveTagHelper("three"), + factory.Code("four").AsTagHelperPrefixDirective("four")); + var expectedDescriptors = new TagHelperDirectiveDescriptor[] + { + new TagHelperDirectiveDescriptor + { + DirectiveText = "one", + DirectiveType = TagHelperDirectiveType.AddTagHelper + }, + new TagHelperDirectiveDescriptor + { + DirectiveText = "two", + DirectiveType = TagHelperDirectiveType.RemoveTagHelper + }, + new TagHelperDirectiveDescriptor + { + DirectiveText = "three", + DirectiveType = TagHelperDirectiveType.RemoveTagHelper + }, + new TagHelperDirectiveDescriptor + { + DirectiveText = "four", + DirectiveType = TagHelperDirectiveType.TagHelperPrefix + } + }; + + // Act + tagHelperDirectiveSpanVisitor.GetDescriptors(document); + + // Assert + Assert.Equal( + expectedDescriptors, + resolver.DirectiveDescriptors, + TagHelperDirectiveDescriptorComparer.Default); + } + + [Fact] + public void GetDescriptors_CanOverrideResolutionContext() + { + // Arrange + var factory = new SpanFactory(); + var resolver = new TestTagHelperDescriptorResolver(); + var expectedInitialDirectiveDescriptors = new TagHelperDirectiveDescriptor[] + { + new TagHelperDirectiveDescriptor + { + DirectiveText = "one", + DirectiveType = TagHelperDirectiveType.AddTagHelper + }, + new TagHelperDirectiveDescriptor + { + DirectiveText = "two", + DirectiveType = TagHelperDirectiveType.RemoveTagHelper + }, + new TagHelperDirectiveDescriptor + { + DirectiveText = "three", + DirectiveType = TagHelperDirectiveType.RemoveTagHelper + }, + new TagHelperDirectiveDescriptor + { + DirectiveText = "four", + DirectiveType = TagHelperDirectiveType.TagHelperPrefix + } + }; + var expectedEndDirectiveDescriptors = new TagHelperDirectiveDescriptor[] + { + new TagHelperDirectiveDescriptor + { + DirectiveText = "custom", + DirectiveType = TagHelperDirectiveType.AddTagHelper + } + }; + var tagHelperDirectiveSpanVisitor = new CustomTagHelperDirectiveSpanVisitor( + resolver, + (descriptors, errorSink) => + { + Assert.Equal( + expectedInitialDirectiveDescriptors, + descriptors, + TagHelperDirectiveDescriptorComparer.Default); + + return new TagHelperDescriptorResolutionContext(expectedEndDirectiveDescriptors, errorSink); + }); + var document = new MarkupBlock( + factory.Code("one").AsAddTagHelper("one"), + factory.Code("two").AsRemoveTagHelper("two"), + factory.Code("three").AsRemoveTagHelper("three"), + factory.Code("four").AsTagHelperPrefixDirective("four")); + + + // Act + tagHelperDirectiveSpanVisitor.GetDescriptors(document); + + // Assert + Assert.Equal(expectedEndDirectiveDescriptors, + resolver.DirectiveDescriptors, + TagHelperDirectiveDescriptorComparer.Default); + } + + [Fact] + public void GetDescriptors_LocatesTagHelperPrefixDirectiveChunkGenerator() + { + // Arrange + var factory = new SpanFactory(); + var resolver = new TestTagHelperDescriptorResolver(); + var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor(resolver, new ErrorSink()); + var document = new MarkupBlock( + new DirectiveBlock( + factory.CodeTransition(), + factory + .MetaCode(SyntaxConstants.CSharp.TagHelperPrefixKeyword + " ") + .Accepts(AcceptedCharacters.None), + factory.Code("something").AsTagHelperPrefixDirective("something"))); + var expectedDirectiveDescriptor = + new TagHelperDirectiveDescriptor + { + DirectiveText = "something", + DirectiveType = TagHelperDirectiveType.TagHelperPrefix + }; + + // Act + tagHelperDirectiveSpanVisitor.GetDescriptors(document); + + // Assert + var directiveDescriptor = Assert.Single(resolver.DirectiveDescriptors); + Assert.Equal( + expectedDirectiveDescriptor, + directiveDescriptor, + TagHelperDirectiveDescriptorComparer.Default); + } + + [Fact] + public void GetDescriptors_LocatesAddTagHelperChunkGenerator() + { + // Arrange + var factory = new SpanFactory(); + var resolver = new TestTagHelperDescriptorResolver(); + var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor(resolver, new ErrorSink()); + var document = new MarkupBlock( + new DirectiveBlock( + factory.CodeTransition(), + factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + factory.Code("something").AsAddTagHelper("something")) + ); + var expectedRegistration = new TagHelperDirectiveDescriptor + { + DirectiveText = "something", + DirectiveType = TagHelperDirectiveType.AddTagHelper + }; + + // Act + tagHelperDirectiveSpanVisitor.GetDescriptors(document); + + // Assert + var directiveDescriptor = Assert.Single(resolver.DirectiveDescriptors); + Assert.Equal(expectedRegistration, directiveDescriptor, TagHelperDirectiveDescriptorComparer.Default); + } + + [Fact] + public void GetDescriptors_LocatesNestedRemoveTagHelperChunkGenerator() + { + // Arrange + var factory = new SpanFactory(); + var resolver = new TestTagHelperDescriptorResolver(); + var tagHelperDirectiveSpanVisitor = new TagHelperDirectiveSpanVisitor(resolver, new ErrorSink()); + var document = new MarkupBlock( + new DirectiveBlock( + factory.CodeTransition(), + factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + factory.Code("something").AsRemoveTagHelper("something")) + ); + var expectedRegistration = new TagHelperDirectiveDescriptor + { + DirectiveText = "something", + DirectiveType = TagHelperDirectiveType.RemoveTagHelper + }; + + // Act + tagHelperDirectiveSpanVisitor.GetDescriptors(document); + + // Assert + var directiveDescriptor = Assert.Single(resolver.DirectiveDescriptors); + Assert.Equal(expectedRegistration, directiveDescriptor, TagHelperDirectiveDescriptorComparer.Default); + } + + [Fact] + public void GetDescriptors_RemoveTagHelperNotInDocument_DoesNotThrow() + { + // Arrange + var factory = new SpanFactory(); + var tagHelperDirectiveSpanVisitor = + new TagHelperDirectiveSpanVisitor( + new TestTagHelperDescriptorResolver(), + new ErrorSink()); + var document = new MarkupBlock(factory.Markup("Hello World")); + + // Act + var descriptors = tagHelperDirectiveSpanVisitor.GetDescriptors(document); + + Assert.Empty(descriptors); + } + + private class TestTagHelperDescriptorResolver : ITagHelperDescriptorResolver + { + public TestTagHelperDescriptorResolver() + { + DirectiveDescriptors = new List(); + } + + public List DirectiveDescriptors { get; } + + public IEnumerable Resolve(TagHelperDescriptorResolutionContext resolutionContext) + { + DirectiveDescriptors.AddRange(resolutionContext.DirectiveDescriptors); + + return Enumerable.Empty(); + } + } + + private class TagHelperDirectiveDescriptorComparer : IEqualityComparer + { + public static readonly TagHelperDirectiveDescriptorComparer Default = + new TagHelperDirectiveDescriptorComparer(); + + private TagHelperDirectiveDescriptorComparer() + { + } + + public bool Equals(TagHelperDirectiveDescriptor directiveDescriptorX, + TagHelperDirectiveDescriptor directiveDescriptorY) + { + return string.Equals(directiveDescriptorX.DirectiveText, + directiveDescriptorY.DirectiveText, + StringComparison.Ordinal) && + directiveDescriptorX.DirectiveType == directiveDescriptorY.DirectiveType; + } + + public int GetHashCode(TagHelperDirectiveDescriptor directiveDescriptor) + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(base.GetHashCode()); + hashCodeCombiner.Add(directiveDescriptor.DirectiveText); + hashCodeCombiner.Add(directiveDescriptor.DirectiveType); + + return hashCodeCombiner; + } + } + + private class CustomTagHelperDirectiveSpanVisitor : TagHelperDirectiveSpanVisitor + { + private Func, + ErrorSink, + TagHelperDescriptorResolutionContext> _replacer; + + public CustomTagHelperDirectiveSpanVisitor( + ITagHelperDescriptorResolver descriptorResolver, + Func, + ErrorSink, + TagHelperDescriptorResolutionContext> replacer) + : base(descriptorResolver, new ErrorSink()) + { + _replacer = replacer; + } + + protected override TagHelperDescriptorResolutionContext GetTagHelperDescriptorResolutionContext( + IEnumerable descriptors, + ErrorSink errorSink) + { + return _replacer(descriptors, errorSink); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperParseTreeRewriterTest.cs new file mode 100644 index 0000000000..baba797bfe --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperParseTreeRewriterTest.cs @@ -0,0 +1,4499 @@ +// 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 TagHelperParseTreeRewriterTest : TagHelperRewritingTestBase + { + public static TheoryData GetAttributeNameValuePairsData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + Func> kvp = + (key, value) => new KeyValuePair(key, value); + var empty = Enumerable.Empty>(); + var csharp = TagHelperParseTreeRewriter.InvalidAttributeValueMarker; + + // documentContent, expectedPairs + return new TheoryData>> + { + { "", empty }, + { "", empty }, + { "", new[] { kvp("href", csharp) } }, + { "", new[] { kvp("href", $"prefix{csharp} suffix") } }, + { "", new[] { kvp("href", "~/home") } }, + { "", new[] { kvp("href", "~/home"), kvp("", "") } }, + { + "", + new[] { kvp("href", $"{csharp}::0"), kvp("class", "btn btn-success"), kvp("random", "") } + }, + { "", new[] { kvp("href", "") } }, + { "> expectedPairs) + { + // Arrange + var errorSink = new ErrorSink(); + var parseResult = ParseDocument(documentContent); + var document = parseResult.Root; + var parseTreeRewriter = new TagHelperParseTreeRewriter(provider: null); + + // Assert - Guard + var rootBlock = Assert.IsType(document); + var child = Assert.Single(rootBlock.Children); + var tagBlock = Assert.IsType(child); + Assert.Equal(BlockType.Tag, tagBlock.Type); + Assert.Empty(errorSink.Errors); + + // Act + var pairs = parseTreeRewriter.GetAttributeNameValuePairs(tagBlock); + + // Assert + Assert.Equal(expectedPairs, pairs); + } + + public static TheoryData PartialRequiredParentData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + Func errorFormatUnclosed = (location, tagName) => + new RazorError( + $"Found a malformed '{tagName}' tag helper. Tag helpers must have a start and end tag or be " + + "self closing.", + new SourceLocation(location, 0, location), + tagName.Length); + Func errorFormatNoCloseAngle = (location, tagName) => + new RazorError( + $"Missing close angle for tag helper '{tagName}'.", + new SourceLocation(location, 0, location), + tagName.Length); + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong"))), + new[] { errorFormatUnclosed(1, "p"), errorFormatUnclosed(4, "strong") } + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong"))), + new[] { errorFormatUnclosed(1, "p") } + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong")), + blockFactory.MarkupTagBlock("")), + new[] { errorFormatUnclosed(4, "strong") } + }, + { + "<

      <

      ", + new MarkupBlock( + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("strong", + blockFactory.MarkupTagBlock(""))), + new[] { errorFormatNoCloseAngle(17, "strong"), errorFormatUnclosed(25, "strong") } + }, + { + "<

      <

      ", + new MarkupBlock( + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("strong", + blockFactory.MarkupTagBlock(""))), + new[] { errorFormatUnclosed(26, "strong") } + }, + + { + "<

      <

      ", + new MarkupBlock( + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("custom", + blockFactory.MarkupTagBlock(""))), + new[] { errorFormatUnclosed(27, "custom") } + }, + }; + } + } + + [Theory] + [MemberData(nameof(PartialRequiredParentData))] + public void Rewrite_UnderstandsPartialRequiredParentTags( + string documentContent, + object expectedOutput, + object expectedErrors) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly", + RequiredParent = "p", + }, + new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly", + RequiredParent = "div", + }, + new TagHelperDescriptor + { + TagName = "*", + TypeName = "CatchALlTagHelper", + AssemblyName = "SomeAssembly", + RequiredParent = "p", + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper", + AssemblyName = "SomeAssembly" + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors); + } + + public static TheoryData NestedVoidSelfClosingRequiredParentData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + // documentContent, expectedOutput + return new TheoryData + { + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("input", TagMode.StartTagOnly), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("input", TagMode.StartTagOnly), + new MarkupTagHelperBlock("strong"))) + }, + { + "


      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("
      "), + new MarkupTagHelperBlock("strong"))) + }, + { + "


      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("
      ")), + new MarkupTagHelperBlock("strong"))) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("input", TagMode.StartTagOnly), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("input", TagMode.SelfClosing), + new MarkupTagHelperBlock("strong", TagMode.SelfClosing))) + }, + { + "


      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("
      "), + new MarkupTagHelperBlock("strong", TagMode.SelfClosing))) + }, + { + "


      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("
      ")), + new MarkupTagHelperBlock("strong", TagMode.SelfClosing))) + }, + }; + } + } + + [Theory] + [MemberData(nameof(NestedVoidSelfClosingRequiredParentData))] + public void Rewrite_UnderstandsNestedVoidSelfClosingRequiredParent( + string documentContent, + object expectedOutput) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "input", + TypeName = "InputTagHelper", + AssemblyName = "SomeAssembly", + TagStructure = TagStructure.WithoutEndTag, + }, + new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly", + RequiredParent = "p", + }, + new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly", + RequiredParent = "input", + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper", + AssemblyName = "SomeAssembly" + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, (MarkupBlock)expectedOutput, expectedErrors: new RazorError[0]); + } + + public static TheoryData NestedRequiredParentData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + // documentContent, expectedOutput + return new TheoryData + { + { + "", + new MarkupBlock( + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong"))) + }, + { + "
      ", + new MarkupBlock( + blockFactory.MarkupTagBlock("
      "), + new MarkupTagHelperBlock("strong"), + blockFactory.MarkupTagBlock("
      ")) + }, + { + "", + new MarkupBlock( + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("")))) + }, + }; + } + } + + [Theory] + [MemberData(nameof(NestedRequiredParentData))] + public void Rewrite_UnderstandsNestedRequiredParent(string documentContent, object expectedOutput) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly", + RequiredParent = "p", + }, + new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly", + RequiredParent = "div", + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper", + AssemblyName = "SomeAssembly" + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, (MarkupBlock)expectedOutput, expectedErrors: new RazorError[0]); + } + + [Fact] + public void Rewrite_UnderstandsTagHelperPrefixAndAllowedChildren() + { + // Arrange + var documentContent = ""; + var expectedOutput = new MarkupBlock( + new MarkupTagHelperBlock("th:p", + new MarkupTagHelperBlock("th:strong"))); + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper", + AssemblyName = "SomeAssembly", + AllowedChildren = new[] { "strong" }, + Prefix = "th:" + }, + new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly", + Prefix = "th:" + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData( + descriptorProvider, + documentContent, + expectedOutput, + expectedErrors: Enumerable.Empty()); + } + + public static TheoryData InvalidHtmlScriptBlockData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + return new TheoryData + { + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("")), + factory.Markup(""), + blockFactory.MarkupTagBlock("")) + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" types='", 7, 0, 7), + suffix: new LocationTagged("'", 24, 0, 24)), + factory.Markup(" types='").With(SpanChunkGenerator.Null), + factory.Markup("text/html").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 15, 0, 15), + value: new LocationTagged("text/html", 15, 0, 15))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + factory.Markup(""), + blockFactory.MarkupTagBlock("")) + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" type='", 7, 0, 7), + suffix: new LocationTagged("'", 31, 0, 31)), + factory.Markup(" type='").With(SpanChunkGenerator.Null), + factory.Markup("text/html").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 14, 0, 14), + value: new LocationTagged("text/html", 14, 0, 14))), + factory.Markup(" invalid").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(" ", 23, 0, 23), + value: new LocationTagged("invalid", 24, 0, 24))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + factory.Markup(""), + blockFactory.MarkupTagBlock("")) + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" type='", 7, 0, 7), + suffix: new LocationTagged("'", 23, 0, 23)), + factory.Markup(" type='").With(SpanChunkGenerator.Null), + factory.Markup("text/ng-*").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 14, 0, 14), + value: new LocationTagged("text/ng-*", 14, 0, 14))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "type", + prefix: new LocationTagged(" type='", 24, 0, 24), + suffix: new LocationTagged("'", 40, 0, 40)), + factory.Markup(" type='").With(SpanChunkGenerator.Null), + factory.Markup("text/html").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 31, 0, 31), + value: new LocationTagged("text/html", 31, 0, 31))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + factory.Markup(""), + blockFactory.MarkupTagBlock("")) + }, + }; + } + } + + [Theory] + [MemberData(nameof(InvalidHtmlScriptBlockData))] + public void TagHelperParseTreeRewriter_DoesNotUnderstandTagHelpersInInvalidHtmlTypedScriptTags( + string documentContent, + object expectedOutput) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "input"); + } + + public static TheoryData HtmlScriptBlockData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + return new TheoryData + { + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" type='", 7, 0, 7), + suffix: new LocationTagged("'", 23, 0, 23)), + factory.Markup(" type='").With(SpanChunkGenerator.Null), + factory.Markup("text/html").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 14, 0, 14), + value: new LocationTagged("text/html", 14, 0, 14))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + new MarkupTagHelperBlock("input", TagMode.SelfClosing), + blockFactory.MarkupTagBlock("")) + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" id='", 7, 0, 7), + suffix: new LocationTagged("'", 21, 0, 21)), + factory.Markup(" id='").With(SpanChunkGenerator.Null), + factory.Markup("scriptTag").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 12, 0, 12), + value: new LocationTagged("scriptTag", 12, 0, 12))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "type", + prefix: new LocationTagged(" type='", 22, 0, 22), + suffix: new LocationTagged("'", 38, 0, 38)), + factory.Markup(" type='").With(SpanChunkGenerator.Null), + factory.Markup("text/html").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 29, 0, 29), + value: new LocationTagged("text/html", 29, 0, 29))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class='", 39, 0, 39), + suffix: new LocationTagged("'", 56, 0, 56)), + factory.Markup(" class='").With(SpanChunkGenerator.Null), + factory.Markup("something").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 47, 0, 47), + value: new LocationTagged("something", 47, 0, 47))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + new MarkupTagHelperBlock("input", TagMode.SelfClosing), + blockFactory.MarkupTagBlock("")) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" type='", 7, 0, 7), + suffix: new LocationTagged("'", 23, 0, 23)), + factory.Markup(" type='").With(SpanChunkGenerator.Null), + factory.Markup("text/html").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 14, 0, 14), + value: new LocationTagged("text/html", 14, 0, 14))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + new MarkupTagHelperBlock("p", + new MarkupTagBlock( + factory.Markup("(" type='", 35, 0, 35), + suffix: new LocationTagged("'", 51, 0, 51)), + factory.Markup(" type='").With(SpanChunkGenerator.Null), + factory.Markup("text/html").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 42, 0, 42), + value: new LocationTagged("text/html", 42, 0, 42))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + new MarkupTagHelperBlock("input", TagMode.SelfClosing), + blockFactory.MarkupTagBlock("")), + blockFactory.MarkupTagBlock("")) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" type='", 7, 0, 7), + suffix: new LocationTagged("'", 23, 0, 23)), + factory.Markup(" type='").With(SpanChunkGenerator.Null), + factory.Markup("text/html").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 14, 0, 14), + value: new LocationTagged("text/html", 14, 0, 14))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + new MarkupTagHelperBlock("p", + new MarkupTagBlock( + factory.Markup("(" type='", 35, 0, 35), + suffix: new LocationTagged("'", 52, 0, 52)), + factory.Markup(" type='").With(SpanChunkGenerator.Null), + factory.Markup("text/").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 42, 0, 42), + value: new LocationTagged("text/", 42, 0, 42))), + factory.Markup(" html").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(" ", 47, 0, 47), + value: new LocationTagged("html", 48, 0, 48))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + factory.Markup(""), + blockFactory.MarkupTagBlock("")), + blockFactory.MarkupTagBlock("")) + }, + }; + } + } + + [Theory] + [MemberData(nameof(HtmlScriptBlockData))] + public void TagHelperParseTreeRewriter_UnderstandsTagHelpersInHtmlTypedScriptTags( + string documentContent, + object expectedOutput) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p", "input"); + } + + [Fact] + public void Rewrite_CanHandleInvalidChildrenWithWhitespace() + { + // Arrange + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var documentContent = $"

      {Environment.NewLine} {Environment.NewLine} Hello" + + $"{Environment.NewLine} {Environment.NewLine}

      "; + var newLineLength = Environment.NewLine.Length; + var expectedErrors = new[] { + new RazorError( + LegacyResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag("strong", "p", "br"), + absoluteIndex: 8 + newLineLength, + lineIndex: 1, + columnIndex: 5, + length: 6), + }; + var expectedOutput = new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup(Environment.NewLine + " "), + blockFactory.MarkupTagBlock(""), + factory.Markup(Environment.NewLine + " Hello" + Environment.NewLine + " "), + blockFactory.MarkupTagBlock(""), + factory.Markup(Environment.NewLine))); + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper", + AssemblyName = "SomeAssembly", + AllowedChildren = new[] { "br" }, + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors); + } + + [Fact] + public void Rewrite_RecoversWhenRequiredAttributeMismatchAndRestrictedChildren() + { + // Arrange + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var documentContent = ""; + + var expectedErrors = new[] { + new RazorError( + LegacyResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag("strong", "strong", "br"), + absoluteIndex: 18, + lineIndex: 0, + columnIndex: 18, + length: 6) + }; + var expectedOutput = new MarkupBlock( + new MarkupTagHelperBlock("strong", + new List + { + new TagHelperAttributeNode("required", null, HtmlAttributeValueStyle.Minimized) + }, + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""))); + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly", + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "required" } }, + AllowedChildren = new[] { "br" } + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors); + } + + [Fact] + public void Rewrite_CanHandleMultipleTagHelpersWithAllowedChildren_OneNull() + { + // Arrange + var factory = new SpanFactory(); + var documentContent = "

      Hello World

      "; + var expectedOutput = new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", + factory.Markup("Hello World")), + new MarkupTagHelperBlock("br", TagMode.StartTagOnly))); + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper1", + AssemblyName = "SomeAssembly", + AllowedChildren = new[] { "strong", "br" } + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper2", + AssemblyName = "SomeAssembly" + }, + new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly" + }, + new TagHelperDescriptor + { + TagName = "br", + TypeName = "BRTagHelper", + AssemblyName = "SomeAssembly", + TagStructure = TagStructure.WithoutEndTag + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]); + } + + [Fact] + public void Rewrite_CanHandleMultipleTagHelpersWithAllowedChildren() + { + // Arrange + var factory = new SpanFactory(); + var documentContent = "

      Hello World

      "; + var expectedOutput = new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", + factory.Markup("Hello World")), + new MarkupTagHelperBlock("br", TagMode.StartTagOnly))); + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper1", + AssemblyName = "SomeAssembly", + AllowedChildren = new[] { "strong" } + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper2", + AssemblyName = "SomeAssembly", + AllowedChildren = new[] { "br" } + }, + new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly" + }, + new TagHelperDescriptor + { + TagName = "br", + TypeName = "BRTagHelper", + AssemblyName = "SomeAssembly", + TagStructure = TagStructure.WithoutEndTag + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]); + } + + public static TheoryData AllowedChildrenData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + Func nestedTagError = + (childName, parentName, allowed, location, length) => new RazorError( + LegacyResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag( + childName, + parentName, + allowed), + absoluteIndex: location, + lineIndex: 0, + columnIndex: location, + length: length); + Func nestedContentError = + (parentName, allowed, location, length) => new RazorError( + LegacyResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent(parentName, allowed), + absoluteIndex: location, + lineIndex: 0, + columnIndex: location, + length: length); + + return new TheoryData, MarkupBlock, RazorError[]> + { + { + "


      ", + new[] { "br" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("br", TagMode.SelfClosing))), + new RazorError[0] + }, + { + $"

      {Environment.NewLine}
      {Environment.NewLine}

      ", + new[] { "br" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup(Environment.NewLine), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup(Environment.NewLine))), + new RazorError[0] + }, + { + "


      ", + new[] { "strong" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("br", TagMode.StartTagOnly))), + new[] { nestedTagError("br", "p", "strong", 4, 2) } + }, + { + "

      Hello

      ", + new[] { "strong" }, + new MarkupBlock(new MarkupTagHelperBlock("p", factory.Markup("Hello"))), + new[] { nestedContentError("p", "strong", 3, 5) } + }, + { + "


      ", + new[] { "br", "strong" }, + new MarkupBlock(new MarkupTagHelperBlock("p", blockFactory.MarkupTagBlock("
      "))), + new[] { nestedTagError("hr", "p", "br, strong", 4, 2) } + }, + { + "


      Hello

      ", + new[] { "strong" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("br", TagMode.StartTagOnly), + factory.Markup("Hello"))), + new[] { nestedTagError("br", "p", "strong", 4, 2), nestedContentError("p", "strong", 7, 5) } + }, + { + "

      Title:
      Something

      ", + new[] { "strong" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", factory.Markup("Title:")), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup("Something"))), + new[] + { + nestedContentError("strong", "strong", 11, 6), + nestedTagError("br", "p", "strong", 27, 2), + nestedContentError("p", "strong", 32, 9), + } + }, + { + "

      Title:
      Something

      ", + new[] { "strong", "br" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", factory.Markup("Title:")), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup("Something"))), + new[] + { + nestedContentError("strong", "strong, br", 11, 6), + nestedContentError("p", "strong, br", 32, 9), + } + }, + { + "

      Title:
      Something

      ", + new[] { "strong", "br" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup(" "), + new MarkupTagHelperBlock("strong", factory.Markup("Title:")), + factory.Markup(" "), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup(" Something"))), + new[] + { + nestedContentError("strong", "strong, br", 13, 6), + nestedContentError("p", "strong, br", 38, 9), + } + }, + { + "

      Title:
      A Very Cool

      Something

      ", + new[] { "strong" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", + factory.Markup("Title:"), + new MarkupTagHelperBlock("br", TagMode.StartTagOnly), + blockFactory.MarkupTagBlock(""), + factory.Markup("A Very Cool"), + blockFactory.MarkupTagBlock("")), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup("Something"))), + new[] + { + nestedContentError("strong", "strong", 11, 6), + nestedTagError("br", "strong", "strong", 18, 2), + nestedTagError("em", "strong", "strong", 22, 2), + nestedTagError("br", "p", "strong", 51, 2), + nestedContentError("p", "strong", 56, 9) + } + }, + { + "

      Title:
      A Very Cool

      Something

      ", + new[] { "custom" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock(""), + factory.Markup("Title:"), + new MarkupTagHelperBlock("br", TagMode.StartTagOnly), + blockFactory.MarkupTagBlock(""), + factory.Markup("A Very Cool"), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup("Something"))), + new[] + { + nestedTagError("br", "p", "custom", 51, 2), + nestedContentError("p", "custom", 56, 9) + } + }, + { + "

      ", + new[] { "custom" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("<

      ", + new[] { "custom" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock("<"))), + new[] + { + nestedContentError("p", "custom", 3, 1), + } + }, + { + "


      :Hello:

      ", + new[] { "custom", "strong" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock(""), + new MarkupTagHelperBlock("br", TagMode.StartTagOnly), + factory.Markup(":"), + new MarkupTagHelperBlock("strong", + new MarkupTagHelperBlock("strong", + factory.Markup("Hello"))), + factory.Markup(":"), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""))), + new[] + { + nestedContentError("strong", "custom, strong", 32, 5), + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(AllowedChildrenData))] + public void Rewrite_UnderstandsAllowedChildren( + string documentContent, + IEnumerable allowedChildren, + object expectedOutput, + object expectedErrors) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper", + AssemblyName = "SomeAssembly", + AllowedChildren = allowedChildren + }, + new TagHelperDescriptor + { + TagName = "strong", + TypeName = "StrongTagHelper", + AssemblyName = "SomeAssembly", + AllowedChildren = allowedChildren + }, + new TagHelperDescriptor + { + TagName = "br", + TypeName = "BRTagHelper", + AssemblyName = "SomeAssembly", + TagStructure = TagStructure.WithoutEndTag + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors); + } + + [Fact] + public void Rewrite_UnderstandsNullTagNameWithAllowedChildrenForCatchAll() + { + // Arrange + var documentContent = "

      "; + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper", + AssemblyName = "SomeAssembly", + AllowedChildren = new[] { "custom" }, + }, + new TagHelperDescriptor + { + TagName = "*", + TypeName = "CatchAllTagHelper", + AssemblyName = "SomeAssembly", + } + }; + var expectedOutput = new MarkupBlock( + new MarkupTagHelperBlock("p", + BlockFactory.MarkupTagBlock(""; + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper", + AssemblyName = "SomeAssembly", + AllowedChildren = new[] { "custom" }, + Prefix = "th:", + }, + new TagHelperDescriptor + { + TagName = "*", + TypeName = "CatchAllTagHelper", + AssemblyName = "SomeAssembly", + Prefix = "th:", + } + }; + var expectedOutput = new MarkupBlock( + new MarkupTagHelperBlock("th:p", + BlockFactory.MarkupTagBlock(""; + var expectedOutput = new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.StartTagOnly)); + 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, expectedOutput, expectedErrors: new RazorError[0]); + } + + [Fact] + public void Rewrite_CreatesErrorForWithoutEndTagTagStructureForEndTags() + { + // Arrange + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var expectedError = new RazorError( + LegacyResources.FormatTagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag( + "input", + "InputTagHelper", + TagStructure.WithoutEndTag), + absoluteIndex: 2, + lineIndex: 0, + columnIndex: 2, + length: 5); + var documentContent = ""; + var expectedOutput = new MarkupBlock(blockFactory.MarkupTagBlock("")); + 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, expectedOutput, expectedErrors: new[] { expectedError }); + } + + [Fact] + public void Rewrite_CreatesErrorForInconsistentTagStructures() + { + // Arrange + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var expectedError = new RazorError( + LegacyResources.FormatTagHelperParseTreeRewriter_InconsistentTagStructure( + "InputTagHelper1", + "InputTagHelper2", + "input", + nameof(TagHelperDescriptor.TagStructure)), + absoluteIndex: 0, + lineIndex: 0, + columnIndex: 0, + length: 7); + var documentContent = ""; + var expectedOutput = new MarkupBlock(new MarkupTagHelperBlock("input", TagMode.StartTagOnly)); + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "input", + TypeName = "InputTagHelper1", + AssemblyName = "SomeAssembly", + TagStructure = TagStructure.WithoutEndTag + }, + new TagHelperDescriptor + { + TagName = "input", + TypeName = "InputTagHelper2", + AssemblyName = "SomeAssembly", + TagStructure = TagStructure.NormalOrSelfClosing + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new[] { expectedError }); + } + + public static TheoryData RequiredAttributeData + { + 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))))); + + // documentContent, expectedOutput + return new TheoryData + { + { + "

      ", + new MarkupBlock(blockFactory.MarkupTagBlock("

      ")) + }, + { + "

      ", + new MarkupBlock( + blockFactory.MarkupTagBlock("

      "), + blockFactory.MarkupTagBlock("

      ")) + }, + { + "
      ", + new MarkupBlock(blockFactory.MarkupTagBlock("
      ")) + }, + { + "
      ", + new MarkupBlock( + blockFactory.MarkupTagBlock("
      "), + blockFactory.MarkupTagBlock("
      ")) + }, + { + "

      ", + 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("

      "), + 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(""), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("

      "), + })) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + }, + children: new MarkupTagHelperBlock( + "strong", + attributes: new List + { + new TagHelperAttributeNode("catchAll", factory.Markup("hi")) + }, + children: new[] + { + blockFactory.MarkupTagBlock("

      "), + blockFactory.MarkupTagBlock("

      ") + }))) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock( + "strong", + attributes: new List + { + new TagHelperAttributeNode("catchAll", factory.Markup("hi")) + }, + children: new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + }, + children: new[] + { + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + }))) + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + }, + children: 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 MarkupTagHelperBlock( + "strong", + attributes: new List + { + new TagHelperAttributeNode("catchAll", factory.Markup("hi")) + }, + children: new[] + { + 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 MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + }, + children: new[] + { + blockFactory.MarkupTagBlock("

      "), + blockFactory.MarkupTagBlock("

      ") + }), + blockFactory.MarkupTagBlock("

      "), + blockFactory.MarkupTagBlock("

      "), + })) + }, + { + "" + + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "strong", + attributes: new List + { + new TagHelperAttributeNode("catchAll", factory.Markup("hi")) + }, + children: new[] + { + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + new MarkupTagHelperBlock( + "strong", + attributes: new List + { + new TagHelperAttributeNode("catchAll", factory.Markup("hi")) + }, + children: new[] + { + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + }), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + })) + }, + }; + } + } + + [Theory] + [MemberData(nameof(NestedRequiredAttributeData))] + public void Rewrite_NestedRequiredAttributeDescriptorsCreateTagHelperBlocksCorrectly( + 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 = "*", + 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 MalformedRequiredAttributeData + { + 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}'."; + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + " + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + })), + 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("notRequired", factory.Markup("hi")), + new TagHelperAttributeNode("class", factory.Markup("btn")) + })), + 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) + } + }, + { + "

      "), + blockFactory.MarkupTagBlock(" + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + })), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + new SourceLocation(17, 0, 17), + length: 1) + } + }, + { + "

      + { + new TagHelperAttributeNode("notRequired", factory.Markup("hi")), + new TagHelperAttributeNode("class", factory.Markup("btn")) + })), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + new SourceLocation(34, 0, 34), + length: 1) + } + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + attributes: new List + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + }, + children: blockFactory.MarkupTagBlock("

      "))), + 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", + attributes: new List + { + new TagHelperAttributeNode("notRequired", factory.Markup("hi")), + new TagHelperAttributeNode("class", factory.Markup("btn")) + }, + children: blockFactory.MarkupTagBlock("

      "))), + 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("class", factory.Markup("btn")) + })), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + new SourceLocation(1, 0, 1), + length: 1), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + new SourceLocation(17, 0, 17), + length: 1) + } + }, + { + "

      + { + new TagHelperAttributeNode("notRequired", factory.Markup("hi")), + new TagHelperAttributeNode("class", factory.Markup("btn")) + })), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + new SourceLocation(1, 0, 1), + length: 1), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + new SourceLocation(34, 0, 34), + length: 1) + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(MalformedRequiredAttributeData))] + public void Rewrite_RequiredAttributeDescriptorsCreateMalformedTagHelperBlocksCorrectly( + string documentContent, + object expectedOutput, + object expectedErrors) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + TagName = "p", + TypeName = "pTagHelper", + AssemblyName = "SomeAssembly", + RequiredAttributes = new[] { new TagHelperRequiredAttributeDescriptor { Name = "class" } } + } + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors); + } + + public static TheoryData PrefixedTagHelperBoundData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var availableDescriptorsColon = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + Prefix = "th:", + TagName = "myth", + TypeName = "mythTagHelper", + AssemblyName = "SomeAssembly" + }, + new TagHelperDescriptor + { + Prefix = "th:", + TagName = "myth2", + TypeName = "mythTagHelper2", + AssemblyName = "SomeAssembly", + Attributes = new [] + { + new TagHelperAttributeDescriptor + { + Name = "bound", + PropertyName = "Bound", + TypeName = typeof(bool).FullName + } + } + } + }; + var availableDescriptorsText = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + Prefix = "PREFIX", + TagName = "myth", + TypeName = "mythTagHelper", + AssemblyName = "SomeAssembly" + }, + new TagHelperDescriptor + { + Prefix = "PREFIX", + TagName = "myth2", + TypeName = "mythTagHelper2", + AssemblyName = "SomeAssembly", + Attributes = new [] + { + new TagHelperAttributeDescriptor + { + Name = "bound", + PropertyName = "Bound", + TypeName = typeof(bool).FullName + }, + } + } + }; + var availableDescriptorsCatchAll = new TagHelperDescriptor[] + { + new TagHelperDescriptor + { + Prefix = "myth", + TagName = "*", + TypeName = "mythTagHelper", + AssemblyName = "SomeAssembly" + } + }; + + // documentContent, expectedOutput, availableDescriptors + return new TheoryData> + { + { + "", + new MarkupBlock(blockFactory.MarkupTagBlock("")), + availableDescriptorsCatchAll + }, + { + "words and spaces", + new MarkupBlock( + blockFactory.MarkupTagBlock(""), + factory.Markup("words and spaces"), + blockFactory.MarkupTagBlock("")), + availableDescriptorsCatchAll + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("th:myth", tagMode: TagMode.SelfClosing)), + availableDescriptorsColon + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("PREFIXmyth", tagMode: TagMode.SelfClosing)), + availableDescriptorsText + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("th:myth")), + availableDescriptorsColon + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("PREFIXmyth")), + availableDescriptorsText + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "th:myth", + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""))), + availableDescriptorsColon + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "PREFIXmyth", + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""))), + availableDescriptorsText + }, + { + "", + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "th:myth />")), + availableDescriptorsColon + }, + { + "", + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "PREFIXmyth />")), + availableDescriptorsText + }, + { + "", + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "th:myth>"), + blockFactory.EscapedMarkupTagBlock("")), + availableDescriptorsColon + }, + { + "", + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "PREFIXmyth>"), + blockFactory.EscapedMarkupTagBlock("")), + availableDescriptorsText + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "th:myth", + tagMode: TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + })), + availableDescriptorsColon + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "PREFIXmyth", + tagMode: TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + })), + availableDescriptorsText + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "th:myth2", + tagMode: TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + })), + availableDescriptorsColon + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "PREFIXmyth2", + tagMode: TagMode.SelfClosing, + attributes: new List + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + })), + availableDescriptorsText + }, + { + "words and spaces", + new MarkupBlock( + new MarkupTagHelperBlock( + "th:myth", + attributes: new List + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + }, + children: factory.Markup("words and spaces"))), + availableDescriptorsColon + }, + { + "words and spaces", + new MarkupBlock( + new MarkupTagHelperBlock( + "PREFIXmyth", + attributes: new List + { + new TagHelperAttributeNode("class", factory.Markup("btn")) + }, + children: factory.Markup("words and spaces"))), + availableDescriptorsText + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "th:myth2", + tagMode: TagMode.SelfClosing, + attributes: new List + { + { + new TagHelperAttributeNode( + "bound", + new MarkupBlock( + new MarkupBlock( + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharacters.AnyExceptNewline))))) + } + })), + availableDescriptorsColon + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "PREFIXmyth2", + tagMode: TagMode.SelfClosing, + attributes: new List + { + { + new TagHelperAttributeNode( + "bound", + new MarkupBlock( + new MarkupBlock( + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharacters.AnyExceptNewline))))) + } + })), + availableDescriptorsText + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "PREFIXmyth2", + tagMode: TagMode.SelfClosing, + attributes: new List + { + { + new TagHelperAttributeNode( + "bound", + new MarkupBlock( + new MarkupBlock( + factory.CodeMarkup("@"), + factory + .CodeMarkup("@") + .With(SpanChunkGenerator.Null)), + new MarkupBlock( + factory + .EmptyHtml() + .As(SpanKind.Code) + .AsCodeMarkup(), + new ExpressionBlock( + factory.CSharpCodeMarkup("@"), + factory.CSharpCodeMarkup("DateTime.Now") + .With(new ExpressionChunkGenerator()))))) + } + })), + availableDescriptorsText + }, + }; + } + } + + [Theory] + [MemberData(nameof(PrefixedTagHelperBoundData))] + public void Rewrite_AllowsPrefixedTagHelpers( + string documentContent, + object expectedOutput, + object availableDescriptors) + { + // Arrange + var descriptorProvider = new TagHelperDescriptorProvider((IEnumerable)availableDescriptors); + + // Act & Assert + EvaluateData( + descriptorProvider, + documentContent, + (MarkupBlock)expectedOutput, + expectedErrors: Enumerable.Empty()); + } + + public static TheoryData OptOut_WithAttributeTextTagData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var errorFormatNormalUnclosed = + "The \"{0}\" element was not closed. All elements must be either self-closing or have a " + + "matching end tag."; + var errorMatchingBrace = + "The code block is missing a closing \"}\" character. Make sure you have a matching \"}\" " + + "character for all the \"{\" characters within this block, and that none of the \"}\" " + + "characters are being interpreted as markup."; + + 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()); + }; + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + "@{}", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("text"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 8, 0, 8), + suffix: new LocationTagged("\"", 19, 0, 19)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 16, 0, 16), + value: new LocationTagged("btn", 16, 0, 16))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(">").Accepts(AcceptedCharacters.None)), + factory.Markup("}")))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorFormatNormalUnclosed, "!text"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 5) + } + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("text"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 8, 0, 8), + suffix: new LocationTagged("\"", 19, 0, 19)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 16, 0, 16), + value: new LocationTagged("btn", 16, 0, 16))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(">").Accepts(AcceptedCharacters.None)), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + { + "@{words with spaces}", + buildStatementBlock( + () => new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("text"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 8, 0, 8), + suffix: new LocationTagged("\"", 19, 0, 19)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 16, 0, 16), + value: new LocationTagged("btn", 16, 0, 16))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(">").Accepts(AcceptedCharacters.None)), + factory.Markup("words with spaces"), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("text"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class='", 8, 0, 8), + suffix: new LocationTagged("'", 25, 0, 25)), + factory.Markup(" class='").With(SpanChunkGenerator.Null), + factory.Markup("btn1").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 16, 0, 16), + value: new LocationTagged("btn1", 16, 0, 16))), + factory.Markup(" btn2").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(" ", 20, 0, 20), + value: new LocationTagged("btn2", 21, 0, 21))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class2", + prefix: new LocationTagged(" class2=", 26, 0, 26), + suffix: new LocationTagged(string.Empty, 37, 0, 37)), + factory.Markup(" class2=").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 34, 0, 34), + value: new LocationTagged("btn", 34, 0, 34)))), + factory.Markup(">").Accepts(AcceptedCharacters.None)), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("text"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class='", 8, 0, 8), + suffix: new LocationTagged("'", 39, 0, 39)), + factory.Markup(" class='").With(SpanChunkGenerator.Null), + factory.Markup("btn1").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 16, 0, 16), + value: new LocationTagged("btn1", 16, 0, 16))), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator( + new LocationTagged(" ", 20, 0, 20), 21, 0, 21), + factory.Markup(" ").With(SpanChunkGenerator.Null), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))), + factory.Markup(" btn2").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(" ", 34, 0, 34), + value: new LocationTagged("btn2", 35, 0, 35))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">").Accepts(AcceptedCharacters.None)), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + }; + } + } + + public static TheoryData OptOut_WithBlockTextTagData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var errorFormatMalformed = + "Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self " + + "closing."; + var errorFormatNormalUnclosed = + "The \"{0}\" element was not closed. All elements must be either self-closing or have a " + + "matching end tag."; + var errorFormatNormalNotStarted = + "Encountered end tag \"{0}\" with no matching start tag. Are your start/end tags properly " + + "balanced?"; + var errorMatchingBrace = + "The code block is missing a closing \"}\" character. Make sure you have a matching \"}\" " + + "character for all the \"{\" characters within this block, and that none of the \"}\" " + + "characters are being interpreted as markup."; + + 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()); + }; + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + "@{}", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "text>", AcceptedCharacters.None), + factory.Markup("}")))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorFormatNormalUnclosed, "!text", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 5), + } + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new [] + { + new RazorError( + string.Format(errorFormatNormalNotStarted, "!text", CultureInfo.InvariantCulture), + absoluteIndex: 4, lineIndex: 0, columnIndex: 4, length: 5), + } + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "text>", AcceptedCharacters.None), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + { + "@{words and spaces}", + buildStatementBlock( + () => new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "text>", AcceptedCharacters.None), + factory.Markup("words and spaces"), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "text>", AcceptedCharacters.None), + blockFactory.MarkupTagBlock("", AcceptedCharacters.None))), + new [] + { + new RazorError( + string.Format(errorFormatNormalUnclosed, "!text", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 5), + new RazorError( + string.Format(errorFormatMalformed, "text", CultureInfo.InvariantCulture), + absoluteIndex: 11, lineIndex: 0, columnIndex: 11, length: 4) + } + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + new MarkupTagBlock(factory.MarkupTransition("")), + new MarkupTagBlock( + factory.Markup("").Accepts(AcceptedCharacters.None)))), + new [] + { + new RazorError( + string.Format(errorFormatNormalUnclosed, "text", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 4) + } + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "text>", AcceptedCharacters.None), + new MarkupTagHelperBlock("text"), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + { + "@{}", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagBlock(factory.MarkupTransition("")), + new MarkupTagBlock( + factory.Markup("<").Accepts(AcceptedCharacters.None), + factory.BangEscape(), + factory.Markup("text>").Accepts(AcceptedCharacters.None)), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None), + factory.Markup("}")))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorFormatNormalUnclosed, "text", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 4) + } + }, + { + "@{}", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "text>", AcceptedCharacters.None), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None)), + new MarkupBlock( + blockFactory.MarkupTagBlock("", AcceptedCharacters.None)), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()), + new [] + { + new RazorError( + string.Format(errorFormatNormalNotStarted, "text", CultureInfo.InvariantCulture), + absoluteIndex: 19, lineIndex: 0, columnIndex: 19, length: 4), + new RazorError( + string.Format(errorFormatMalformed, "text", CultureInfo.InvariantCulture), + absoluteIndex: 19, lineIndex: 0, columnIndex: 19, length: 4) + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(OptOut_WithAttributeTextTagData))] + [MemberData(nameof(OptOut_WithBlockTextTagData))] + public void Rewrite_AllowsTagHelperElementOptForCompleteTextTagInCSharpBlock( + string documentContent, + object expectedOutput, + object expectedErrors) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors, "p", "text"); + } + + public static TheoryData OptOut_WithPartialTextTagData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var errorMatchingBrace = + "The code block is missing a closing \"}\" character. Make sure you have a matching \"}\" " + + "character for all the \"{\" characters within this block, and that none of the \"}\" " + + "characters are being interpreted as markup."; + var errorEOFMatchingBrace = + "End of file or an unexpected character was reached before the \"{0}\" tag could be parsed. " + + "Elements inside markup blocks must be complete. They must either be self-closing " + + "(\"
      \") or have matching end tags (\"

      Hello

      \"). If you intended " + + "to display a \"<\" character, use the \"<\" HTML entity."; + + Func, MarkupBlock> buildPartialStatementBlock = (insideBuilder) => + { + return new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + insideBuilder())); + }; + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + "@{ new MarkupBlock(blockFactory.EscapedMarkupTagBlock("<", "text}"))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!text}"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 6) + } + }, + { + "@{ new MarkupBlock( + blockFactory.EscapedMarkupTagBlock( + "<", + "text /", + new MarkupBlock(factory.Markup("}"))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!text"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 5) + } + }, + { + "@{ new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("text"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=", 8, 0, 8), + suffix: new LocationTagged(string.Empty, 16, 0, 16)), + factory.Markup(" class=").With(SpanChunkGenerator.Null), + factory.Markup("}").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 15, 0, 15), + value: new LocationTagged("}", 15, 0, 15))))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!text"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 5) + } + }, + { + "@{ new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("text"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 8, 0, 8), + suffix: new LocationTagged(string.Empty, 20, 0, 20)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn}").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 16, 0, 16), + value: new LocationTagged("btn}", 16, 0, 16))))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!text"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 5) + } + }, + { + "@{ new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("text"), + + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 8, 0, 8), + suffix: new LocationTagged("\"", 19, 0, 19)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 16, 0, 16), + value: new LocationTagged("btn", 16, 0, 16))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + new MarkupBlock(factory.Markup("}"))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!text"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 5) + } + }, + { + "@{ new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("text"), + + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 8, 0, 8), + suffix: new LocationTagged("\"", 19, 0, 19)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 16, 0, 16), + value: new LocationTagged("btn", 16, 0, 16))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(" /"), + new MarkupBlock(factory.Markup("}"))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!text"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 5) + } + } + }; + } + } + + [Theory] + [MemberData(nameof(OptOut_WithPartialTextTagData))] + public void Rewrite_AllowsTagHelperElementOptForIncompleteTextTagInCSharpBlock( + string documentContent, + object expectedOutput, + object expectedErrors) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors, "text"); + } + + public static TheoryData OptOut_WithPartialData_CSharp + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var errorMatchingBrace = + "The code block is missing a closing \"}\" character. Make sure you have a matching \"}\" " + + "character for all the \"{\" characters within this block, and that none of the \"}\" " + + "characters are being interpreted as markup."; + var errorEOFMatchingBrace = + "End of file or an unexpected character was reached before the \"{0}\" tag could be parsed. " + + "Elements inside markup blocks must be complete. They must either be self-closing " + + "(\"
      \") or have matching end tags (\"

      Hello

      \"). If you intended " + + "to display a \"<\" character, use the \"<\" HTML entity."; + + Func, MarkupBlock> buildPartialStatementBlock = (insideBuilder) => + { + return new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + insideBuilder())); + }; + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + "@{ new MarkupBlock(blockFactory.EscapedMarkupTagBlock("<", "}"))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!}"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 2) + } + }, + { + "@{ new MarkupBlock(blockFactory.EscapedMarkupTagBlock("<", "p}"))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!p}"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 3) + } + }, + { + "@{ new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p /", new MarkupBlock(factory.Markup("}"))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!p"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 2) + } + }, + { + "@{ new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=", 5, 0, 5), + suffix: new LocationTagged(string.Empty, 13, 0, 13)), + factory.Markup(" class=").With(SpanChunkGenerator.Null), + factory.Markup("}").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 12, 0, 12), + value: new LocationTagged("}", 12, 0, 12))))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!p"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 2) + } + }, + { + "@{ new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 5, 0, 5), + suffix: new LocationTagged(string.Empty, 17, 0, 17)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn}").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 13, 0, 13), + value: new LocationTagged("btn}", 13, 0, 13))))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!p"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 2) + } + }, + { + "@{ new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 5, 0, 5), + suffix: new LocationTagged(string.Empty, 19, 0, 19)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 13, 0, 13), + value: new LocationTagged("btn", 13, 0, 13))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 16, 0, 16), new LocationTagged("@", 16, 0, 16))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("}").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 18, 0, 18), + value: new LocationTagged("}", 18, 0, 18))))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!p"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 2) + } + }, + { + "@{ new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 5, 0, 5), + suffix: new LocationTagged("\"", 16, 0, 16)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 13, 0, 13), + value: new LocationTagged("btn", 13, 0, 13))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + new MarkupBlock(factory.Markup("}"))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!p"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 2) + } + }, + { + "@{ new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 5, 0, 5), + suffix: new LocationTagged("\"", 16, 0, 16)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 13, 0, 13), + value: new LocationTagged("btn", 13, 0, 13))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(" /"), + new MarkupBlock( + factory.Markup("}"))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!p"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 2) + } + } + }; + } + } + + [Theory] + [MemberData(nameof(OptOut_WithPartialData_CSharp))] + public void Rewrite_AllowsTagHelperElementOptForIncompleteHTMLInCSharpBlock( + string documentContent, + object expectedOutput, + object expectedErrors) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors, "strong", "p"); + } + + public static TheoryData OptOut_WithPartialData_HTML + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + // documentContent, expectedOutput + return new TheoryData + { + { + "(" class=", 3, 0, 3), + suffix: new LocationTagged(string.Empty, 10, 0, 10)), + factory.Markup(" class=").With(SpanChunkGenerator.Null)))) + }, + { + "(" class=\"", 3, 0, 3), + suffix: new LocationTagged(string.Empty, 14, 0, 14)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 11, 0, 11), + value: new LocationTagged("btn", 11, 0, 11)))))) + }, + { + "(" class=\"", 3, 0, 3), + suffix: new LocationTagged("\"", 14, 0, 14)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 11, 0, 11), + value: new LocationTagged("btn", 11, 0, 11))), + factory.Markup("\"").With(SpanChunkGenerator.Null)))) + }, + { + "(" class=\"", 3, 0, 3), + suffix: new LocationTagged("\"", 14, 0, 14)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 11, 0, 11), + value: new LocationTagged("btn", 11, 0, 11))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(" /"))) + } + }; + } + } + + [Theory] + [MemberData(nameof(OptOut_WithPartialData_HTML))] + public void Rewrite_AllowsTagHelperElementOptForIncompleteHTML( + string documentContent, + object expectedOutput) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, new RazorError[0], "strong", "p"); + } + + public static TheoryData OptOut_WithBlockData_CSharp + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var errorFormatMalformed = + "Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self " + + "closing."; + var errorFormatNormalUnclosed = + "The \"{0}\" element was not closed. All elements must be either self-closing or have a " + + "matching end tag."; + var errorFormatNormalNotStarted = + "Encountered end tag \"{0}\" with no matching start tag. Are your start/end tags properly " + + "balanced?"; + var errorMatchingBrace = + "The code block is missing a closing \"}\" character. Make sure you have a matching \"}\" " + + "character for all the \"{\" characters within this block, and that none of the \"}\" " + + "characters are being interpreted as markup."; + + 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()); + }; + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + "@{}", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>", AcceptedCharacters.None), + factory.Markup("}")))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorFormatNormalUnclosed, "!p", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 2), + } + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new [] + { + new RazorError( + string.Format(errorFormatNormalNotStarted, "!p", CultureInfo.InvariantCulture), + absoluteIndex: 4, lineIndex: 0, columnIndex: 4, length: 2), + } + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>", AcceptedCharacters.None), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + { + "@{words and spaces}", + buildStatementBlock( + () => new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>", AcceptedCharacters.None), + factory.Markup("words and spaces"), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + { + "@{

      }", + buildStatementBlock( + () => new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>", AcceptedCharacters.None), + blockFactory.MarkupTagBlock("

      ", AcceptedCharacters.None))), + new [] + { + new RazorError( + string.Format(errorFormatNormalUnclosed, "!p", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 2), + new RazorError( + string.Format(errorFormatMalformed, "p", CultureInfo.InvariantCulture), + absoluteIndex: 8, lineIndex: 0, columnIndex: 8, length: 1) + } + }, + { + "@{

      }", + buildStatementBlock( + () => new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None)))), + new [] + { + new RazorError( + string.Format(errorFormatNormalUnclosed, "p", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 1), + new RazorError( + string.Format(errorFormatMalformed, "p", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 1) + } + }, + { + "@{

      }", + buildStatementBlock( + () => new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.EscapedMarkupTagBlock("<", "p>", AcceptedCharacters.None), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None)))), + new RazorError[0] + }, + { + "@{

      }", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.EscapedMarkupTagBlock("<", "p>", AcceptedCharacters.None), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None), + factory.Markup("}"))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorFormatNormalUnclosed, "p", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 1), + new RazorError( + string.Format(errorFormatMalformed, "p", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 1) + } + }, + { + "@{

      }", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>", AcceptedCharacters.None), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None)), + new MarkupBlock( + blockFactory.MarkupTagBlock("

      ", AcceptedCharacters.None)), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()), + new [] + { + new RazorError( + string.Format(errorFormatNormalNotStarted, "p", CultureInfo.InvariantCulture), + absoluteIndex: 13, lineIndex: 0, columnIndex: 13, length: 1), + new RazorError( + string.Format(errorFormatMalformed, "p", CultureInfo.InvariantCulture), + absoluteIndex: 13, lineIndex: 0, columnIndex: 13, length: 1) + } + }, + { + "@{}", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagHelperBlock("strong", + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new MarkupBlock( + blockFactory.MarkupTagBlock("
      ", AcceptedCharacters.None)), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()), + new [] + { + new RazorError( + string.Format(errorFormatNormalUnclosed, "strong", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 6), + new RazorError( + string.Format(errorFormatMalformed, "strong", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 6), + new RazorError( + string.Format(errorFormatNormalNotStarted, "strong", CultureInfo.InvariantCulture), + absoluteIndex: 17, lineIndex: 0, columnIndex: 17, length: 6), + new RazorError( + string.Format(errorFormatMalformed, "strong", CultureInfo.InvariantCulture), + absoluteIndex: 17, lineIndex: 0, columnIndex: 17, length: 6) + } + }, + { + "@{}", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagHelperBlock("strong")), + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>", AcceptedCharacters.None), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None)), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()), + new RazorError[0] + }, + { + "@{

      }", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None)))), + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>", AcceptedCharacters.None), + blockFactory.MarkupTagBlock("", AcceptedCharacters.None)), + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None)), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()), + new [] + { + new RazorError( + string.Format(errorFormatNormalUnclosed, "p", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 1), + new RazorError( + string.Format(errorFormatMalformed, "p", CultureInfo.InvariantCulture), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 1), + new RazorError( + string.Format(errorFormatMalformed, "strong", CultureInfo.InvariantCulture), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 6), + new RazorError( + string.Format(errorFormatNormalUnclosed, "!p", CultureInfo.InvariantCulture), + absoluteIndex: 24, lineIndex: 0, columnIndex: 24, length: 2), + new RazorError( + string.Format(errorFormatMalformed, "strong", CultureInfo.InvariantCulture), + absoluteIndex: 29, lineIndex: 0, columnIndex: 29, length: 6), + new RazorError( + string.Format(errorFormatNormalNotStarted, "!p", CultureInfo.InvariantCulture), + absoluteIndex: 38, lineIndex: 0, columnIndex: 38, length: 2), + } + }, + }; + } + } + + public static TheoryData OptOut_WithAttributeData_CSharp + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + var errorFormatNormalUnclosed = + "The \"{0}\" element was not closed. All elements must be either self-closing or have a " + + "matching end tag."; + var errorMatchingBrace = + "The code block is missing a closing \"}\" character. Make sure you have a matching \"}\" " + + "character for all the \"{\" characters within this block, and that none of the \"}\" " + + "characters are being interpreted as markup."; + + 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()); + }; + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + "@{}", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 5, 0, 5), + suffix: new LocationTagged("\"", 16, 0, 16)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 13, 0, 13), + value: new LocationTagged("btn", 13, 0, 13))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(">").Accepts(AcceptedCharacters.None)), + factory.Markup("}")))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 1), + new RazorError( + string.Format(errorFormatNormalUnclosed, "!p"), + absoluteIndex: 3, lineIndex: 0, columnIndex: 3, length: 2) + } + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 5, 0, 5), + suffix: new LocationTagged("\"", 16, 0, 16)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 13, 0, 13), + value: new LocationTagged("btn", 13, 0, 13))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(">").Accepts(AcceptedCharacters.None)), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + { + "@{words with spaces}", + buildStatementBlock( + () => new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 5, 0, 5), + suffix: new LocationTagged("\"", 16, 0, 16)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 13, 0, 13), + value: new LocationTagged("btn", 13, 0, 13))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(">").Accepts(AcceptedCharacters.None)), + factory.Markup("words with spaces"), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class='", 5, 0, 5), + suffix: new LocationTagged("'", 22, 0, 22)), + factory.Markup(" class='").With(SpanChunkGenerator.Null), + factory.Markup("btn1").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 13, 0, 13), + value: new LocationTagged("btn1", 13, 0, 13))), + factory.Markup(" btn2").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(" ", 17, 0, 17), + value: new LocationTagged("btn2", 18, 0, 18))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class2", + prefix: new LocationTagged(" class2=", 23, 0, 23), + suffix: new LocationTagged(string.Empty, 34, 0, 34)), + factory.Markup(" class2=").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 31, 0, 31), + value: new LocationTagged("btn", 31, 0, 31)))), + factory.Markup(">").Accepts(AcceptedCharacters.None)), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + { + "@{}", + buildStatementBlock( + () => new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class='", 5, 0, 5), + suffix: new LocationTagged("'", 36, 0, 36)), + factory.Markup(" class='").With(SpanChunkGenerator.Null), + factory.Markup("btn1").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 13, 0, 13), + value: new LocationTagged("btn1", 13, 0, 13))), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator( + new LocationTagged(" ", 17, 0, 17), 18, 0, 18), + factory.Markup(" ").With(SpanChunkGenerator.Null), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))), + factory.Markup(" btn2").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(" ", 31, 0, 31), + value: new LocationTagged("btn2", 32, 0, 32))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">").Accepts(AcceptedCharacters.None)), + blockFactory.EscapedMarkupTagBlock("", AcceptedCharacters.None))), + new RazorError[0] + }, + }; + } + } + + [Theory] + [MemberData(nameof(OptOut_WithBlockData_CSharp))] + [MemberData(nameof(OptOut_WithAttributeData_CSharp))] + public void Rewrite_AllowsTagHelperElementOptOutCSharp( + string documentContent, + object expectedOutput, + object expectedErrors) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors, "strong", "p"); + } + + public static TheoryData OptOut_WithBlockData_HTML + { + 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."; + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + "", + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>")), + new RazorError[0] + }, + { + "", + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("")), + new RazorError[0] + }, + { + "", + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>"), + blockFactory.EscapedMarkupTagBlock("")), + new RazorError[0] + }, + { + "words and spaces", + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>"), + factory.Markup("words and spaces"), + blockFactory.EscapedMarkupTagBlock("")), + new RazorError[0] + }, + { + "

      ", + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>"), + blockFactory.MarkupTagBlock("

      ")), + new [] + { + new RazorError( + string.Format(errorFormatUnclosed, "p", CultureInfo.InvariantCulture), + absoluteIndex: 6, lineIndex: 0, columnIndex: 6, length: 1) + } + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", blockFactory.EscapedMarkupTagBlock(""))), + new [] + { + new RazorError( + string.Format(errorFormatUnclosed, "p", CultureInfo.InvariantCulture), + new SourceLocation(1, 0, 1), + length: 1) + } + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.EscapedMarkupTagBlock("<", "p>"), + blockFactory.EscapedMarkupTagBlock(""))), + new RazorError[0] + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.EscapedMarkupTagBlock("<", "p>"), + blockFactory.EscapedMarkupTagBlock(""))), + new [] + { + new RazorError( + string.Format(errorFormatUnclosed, "p", CultureInfo.InvariantCulture), + new SourceLocation(1, 0, 1), + length: 1) + } + }, + { + "

      ", + new MarkupBlock( + blockFactory.EscapedMarkupTagBlock("<", "p>"), + blockFactory.EscapedMarkupTagBlock(""), + blockFactory.MarkupTagBlock("

      ")), + new [] + { + new RazorError( + string.Format(errorFormatUnclosed, "p", CultureInfo.InvariantCulture), + new SourceLocation(11, 0, 11), + length: 1) + } + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("strong", + blockFactory.EscapedMarkupTagBlock(""))), + new RazorError[0] + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("strong"), + blockFactory.EscapedMarkupTagBlock("<", "p>"), + blockFactory.EscapedMarkupTagBlock("")), + new RazorError[0] + }, + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", + blockFactory.EscapedMarkupTagBlock(""), + blockFactory.EscapedMarkupTagBlock("<", "p>")), + blockFactory.EscapedMarkupTagBlock(""))), + new [] + { + new RazorError( + string.Format(errorFormatUnclosed, "p", CultureInfo.InvariantCulture), + new SourceLocation(1, 0, 1), + length: 1) + } + }, + }; + } + } + + public static TheoryData OptOut_WithAttributeData_HTML + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 3, 0, 3), + suffix: new LocationTagged("\"", 14, 0, 14)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 11, 0, 11), + value: new LocationTagged("btn", 11, 0, 11))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(">"))), + new RazorError[0] + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 3, 0, 3), + suffix: new LocationTagged("\"", 14, 0, 14)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 11, 0, 11), + value: new LocationTagged("btn", 11, 0, 11))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + blockFactory.EscapedMarkupTagBlock("")), + new RazorError[0] + }, + { + "words and spaces", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 3, 0, 3), + suffix: new LocationTagged("\"", 14, 0, 14)), + factory.Markup(" class=\"").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 11, 0, 11), + value: new LocationTagged("btn", 11, 0, 11))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + factory.Markup("words and spaces"), + blockFactory.EscapedMarkupTagBlock("")), + new RazorError[0] + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class='", 3, 0, 3), + suffix: new LocationTagged("'", 20, 0, 20)), + factory.Markup(" class='").With(SpanChunkGenerator.Null), + factory.Markup("btn1").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 11, 0, 11), + value: new LocationTagged("btn1", 11, 0, 11))), + factory.Markup(" btn2").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(" ", 15, 0, 15), + value: new LocationTagged("btn2", 16, 0, 16))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class2", + prefix: new LocationTagged(" class2=", 21, 0, 21), + suffix: new LocationTagged(string.Empty, 32, 0, 32)), + factory.Markup(" class2=").With(SpanChunkGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 29, 0, 29), + value: new LocationTagged("btn", 29, 0, 29)))), + factory.Markup(">")), + blockFactory.EscapedMarkupTagBlock("")), + new RazorError[0] + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockChunkGenerator( + name: "class", + prefix: new LocationTagged(" class='", 3, 0, 3), + suffix: new LocationTagged("'", 34, 0, 34)), + factory.Markup(" class='").With(SpanChunkGenerator.Null), + factory.Markup("btn1").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(string.Empty, 11, 0, 11), + value: new LocationTagged("btn1", 11, 0, 11))), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator( + new LocationTagged(" ", 15, 0, 15), 16, 0, 16), + factory.Markup(" ").With(SpanChunkGenerator.Null), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))), + factory.Markup(" btn2").With( + new LiteralAttributeChunkGenerator( + prefix: new LocationTagged(" ", 29, 0, 29), + value: new LocationTagged("btn2", 30, 0, 30))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(">")), + blockFactory.EscapedMarkupTagBlock("")), + new RazorError[0] + }, + }; + } + } + + [Theory] + [MemberData(nameof(OptOut_WithBlockData_HTML))] + [MemberData(nameof(OptOut_WithAttributeData_HTML))] + public void Rewrite_AllowsTagHelperElementOptOutHTML( + string documentContent, + object expectedOutput, + object expectedErrors) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors, "strong", "p"); + } + + public static IEnumerable TextTagsBlockData + { + get + { + var factory = new SpanFactory(); + + // Should re-write text tags that aren't in C# blocks + yield return new object[] + { + "Hello World", + new MarkupBlock( + new MarkupTagHelperBlock("text", + factory.Markup("Hello World"))) + }; + yield return new object[] + { + "@{Hello World}", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagBlock( + factory.MarkupTransition("")), + factory.Markup("Hello World").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + factory.MarkupTransition(""))), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()) + }; + yield return new object[] + { + "@{

      Hello World

      }", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagBlock( + factory.MarkupTransition("")), + new MarkupTagHelperBlock("p", + factory.Markup("Hello World")), + new MarkupTagBlock( + factory.MarkupTransition(""))), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()) + }; + yield return new object[] + { + "@{

      Hello World

      }", + new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("text", + factory.Markup("Hello World")))), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()) + }; + } + } + + [Theory] + [MemberData(nameof(TextTagsBlockData))] + public void TagHelperParseTreeRewriter_DoesNotRewriteTextTagTransitionTagHelpers( + string documentContent, + object expectedOutput) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p", "text"); + } + + public static IEnumerable SpecialTagsBlockData + { + get + { + var factory = new SpanFactory(); + + yield return new object[] + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("")), + factory.Markup(""), + new MarkupTagBlock( + factory.Markup(""))) + }; + yield return new object[] + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("")), + factory.Markup(""), + new MarkupTagBlock( + factory.Markup(""))) + }; + yield return new object[] + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("")), + factory.Markup(""), + new MarkupTagBlock( + factory.Markup(""))) + }; + yield return new object[] + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("")), + factory.Markup(""), + new MarkupTagBlock( + factory.Markup(""))) + }; + yield return new object[] + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("")), + factory.Markup(""), + new MarkupTagBlock( + factory.Markup(""))) + }; + yield return new object[] + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("")), + factory.Markup(""), + new MarkupTagBlock( + factory.Markup(""))) + }; + yield return new object[] + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("")), + factory.Markup(""), + new MarkupTagBlock( + factory.Markup(""))) + }; + yield return new object[] + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("")), + factory.Markup(""), + new MarkupTagBlock( + factory.Markup(""))) + }; + } + } + + [Theory] + [MemberData(nameof(SpecialTagsBlockData))] + public void TagHelperParseTreeRewriter_DoesNotRewriteSpecialTagTagHelpers( + string documentContent, + object expectedOutput) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "!--", "?xml", "![CDATA[", "!DOCTYPE"); + } + + public static IEnumerable NestedBlockData + { + get + { + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + yield return new object[] + { + "

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("div"))) + }; + yield return new object[] + { + "

      Hello World

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup("Hello World "), + new MarkupTagHelperBlock("div"))) + }; + yield return new object[] + { + "

      Hel

      lo

      World

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup("Hel"), + new MarkupTagHelperBlock("p", + factory.Markup("lo"))), + factory.Markup(" "), + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("div", + factory.Markup("World")))) + }; + yield return new object[] + { + "

      Hello

      World

      ", + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup("Hel"), + blockFactory.MarkupTagBlock(""), + factory.Markup("lo"), + blockFactory.MarkupTagBlock("")), + factory.Markup(" "), + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock(""), + factory.Markup("World"), + blockFactory.MarkupTagBlock(""))) + }; + } + } + + [Theory] + [MemberData(nameof(NestedBlockData))] + public void TagHelperParseTreeRewriter_RewritesNestedTagHelperTagBlocks( + string documentContent, + object expectedOutput) + { + RunParseTreeRewriterTest(documentContent, (MarkupBlock)expectedOutput, "p", "div"); + } + + [Fact] + public void Rewrite_HandlesMalformedNestedNonTagHelperTags_Correctly() + { + var documentContent = "
      @{
      }"; + var expectedOutput = new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("
      ")), + new StatementBlock( + Factory.CodeTransition(), + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("
      ").Accepts(AcceptedCharacters.None))), + Factory.EmptyCSharp().AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml()); + var expectedErrors = new[] + { + new RazorError( + "Encountered end tag \"div\" with no matching start tag. Are your start/end tags properly balanced?", + new SourceLocation(9, 0, 9), + 3), + }; + + RunParseTreeRewriterTest(documentContent, expectedOutput, expectedErrors); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperRequiredAttributeDescriptorTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperRequiredAttributeDescriptorTest.cs new file mode 100644 index 0000000000..1f3ff7e052 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperRequiredAttributeDescriptorTest.cs @@ -0,0 +1,173 @@ +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + public class TagHelperRequiredAttributeDescriptorTest + { + public static TheoryData RequiredAttributeDescriptorData + { + get + { + // requiredAttributeDescriptor, attributeName, attributeValue, expectedResult + return new TheoryData + { + { + new TagHelperRequiredAttributeDescriptor + { + Name = "key" + }, + "KeY", + "value", + true + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "key" + }, + "keys", + "value", + false + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "route-", + NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch, + }, + "ROUTE-area", + "manage", + true + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "route-", + NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch, + }, + "routearea", + "manage", + false + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "route-", + NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch, + }, + "route-", + "manage", + false + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "key", + NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch, + }, + "KeY", + "value", + true + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "key", + NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch, + }, + "keys", + "value", + false + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "key", + NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch, + Value = "value", + ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch, + }, + "key", + "value", + true + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "key", + NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch, + Value = "value", + ValueComparison = TagHelperRequiredAttributeValueComparison.FullMatch, + }, + "key", + "Value", + false + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "class", + NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch, + Value = "btn", + ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch, + }, + "class", + "btn btn-success", + true + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "class", + NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch, + Value = "btn", + ValueComparison = TagHelperRequiredAttributeValueComparison.PrefixMatch, + }, + "class", + "BTN btn-success", + false + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "href", + NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch, + Value = "#navigate", + ValueComparison = TagHelperRequiredAttributeValueComparison.SuffixMatch, + }, + "href", + "/home/index#navigate", + true + }, + { + new TagHelperRequiredAttributeDescriptor + { + Name = "href", + NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch, + Value = "#navigate", + ValueComparison = TagHelperRequiredAttributeValueComparison.SuffixMatch, + }, + "href", + "/home/index#NAVigate", + false + }, + }; + } + } + + [Theory] + [MemberData(nameof(RequiredAttributeDescriptorData))] + public void Matches_ReturnsExpectedResult( + object requiredAttributeDescriptor, + string attributeName, + string attributeValue, + bool expectedResult) + { + // Act + var result = ((TagHelperRequiredAttributeDescriptor)requiredAttributeDescriptor).IsMatch(attributeName, attributeValue); + + // Assert + Assert.Equal(expectedResult, result); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperRewritingTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperRewritingTestBase.cs new file mode 100644 index 0000000000..063def7d7a --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperRewritingTestBase.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Razor.Evolution.TagHelpers; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + public class TagHelperRewritingTestBase : CsHtmlMarkupParserTestBase + { + internal void RunParseTreeRewriterTest( + string documentContent, + MarkupBlock expectedOutput, + params string[] tagNames) + { + RunParseTreeRewriterTest( + documentContent, + expectedOutput, + errors: Enumerable.Empty(), + tagNames: tagNames); + } + + internal void RunParseTreeRewriterTest( + string documentContent, + MarkupBlock expectedOutput, + IEnumerable errors, + params string[] tagNames) + { + var providerContext = BuildProviderContext(tagNames); + + EvaluateData(providerContext, documentContent, expectedOutput, errors); + } + + internal TagHelperDescriptorProvider BuildProviderContext(params string[] tagNames) + { + var descriptors = new List(); + + foreach (var tagName in tagNames) + { + descriptors.Add( + new TagHelperDescriptor + { + TagName = tagName, + TypeName = tagName + "taghelper", + AssemblyName = "SomeAssembly" + }); + } + + return new TagHelperDescriptorProvider(descriptors); + } + + internal void EvaluateData( + TagHelperDescriptorProvider provider, + string documentContent, + MarkupBlock expectedOutput, + IEnumerable expectedErrors) + { + var syntaxTree = ParseDocument(documentContent); + var errorSink = new ErrorSink(); + var parseTreeRewriter = new TagHelperParseTreeRewriter(provider); + var actualTree = parseTreeRewriter.Rewrite(syntaxTree.Root, errorSink); + + var allErrors = syntaxTree.Diagnostics.Concat(errorSink.Errors); + var actualErrors = allErrors + .OrderBy(error => error.Location.AbsoluteIndex) + .ToList(); + + EvaluateRazorErrors(actualErrors, expectedErrors.ToList()); + EvaluateParseTree(actualTree, expectedOutput); + } + } +}