diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs index cbb43762c9..629e955d90 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs @@ -1,14 +1,11 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.Text; -using Microsoft.AspNet.Razor.Tokenizer.Symbols; namespace Microsoft.AspNet.Razor.Parser.TagHelpers { @@ -18,7 +15,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers public class TagHelperBlockBuilder : BlockBuilder { /// - /// Instantiates a new instance based on given the + /// Instantiates a new instance based on the given /// . /// /// The original to copy data from. @@ -36,20 +33,21 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers /// and from the . /// /// An HTML tag name. + /// Starting location of the . + /// Attributes of the . /// The s associated with the current HTML /// tag. - /// The that contains all information about the start - /// of the HTML element. - public TagHelperBlockBuilder(string tagName, IEnumerable descriptors, Block startTag) + public TagHelperBlockBuilder(string tagName, + SourceLocation start, + IDictionary attributes, + IEnumerable descriptors) { TagName = tagName; + Start = start; Descriptors = descriptors; + Type = BlockType.Tag; CodeGenerator = new TagHelperCodeGenerator(descriptors); - Type = startTag.Type; - Attributes = GetTagAttributes(startTag, descriptors); - - // There will always be at least one child for the '<'. - Start = startTag.Children.First().Start; + Attributes = new Dictionary(attributes); } // Internal for testing @@ -113,307 +111,5 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers /// The starting of the tag helper. /// public SourceLocation Start { get; private set; } - - private static IDictionary GetTagAttributes( - Block tagBlock, - IEnumerable descriptors) - { - var attributes = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // Build a dictionary so we can easily lookup expected attribute value lookups - IReadOnlyDictionary attributeValueTypes = - descriptors.SelectMany(descriptor => descriptor.Attributes) - .Distinct(TagHelperAttributeDescriptorComparer.Default) - .ToDictionary(descriptor => descriptor.Name, - descriptor => descriptor.TypeName, - StringComparer.OrdinalIgnoreCase); - - // We skip the first child "" or "/>". - // The -2 accounts for both the start and end tags. - var attributeChildren = tagBlock.Children.Skip(1).Take(tagBlock.Children.Count() - 2); - - foreach (var child in attributeChildren) - { - KeyValuePair attribute; - - if (child.IsBlock) - { - attribute = ParseBlock((Block)child, attributeValueTypes); - } - else - { - attribute = ParseSpan((Span)child, attributeValueTypes); - } - - attributes.Add(attribute.Key, attribute.Value); - } - - return attributes; - } - - // 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 KeyValuePair ParseSpan( - Span span, - IReadOnlyDictionary attributeValueTypes) - { - var afterEquals = false; - var builder = new SpanBuilder - { - CodeGenerator = span.CodeGenerator, - EditHandler = span.EditHandler, - Kind = span.Kind - }; - var htmlSymbols = span.Symbols.OfType().ToArray(); - var capturedAttributeValueStart = false; - var attributeValueStartLocation = span.Start; - var symbolOffset = 1; - 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) - { - // 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 && symbol.Type == HtmlSymbolType.Text) - { - name = symbol.Content; - attributeValueStartLocation = SourceLocation.Advance(span.Start, name); - } - else if (symbol.Type == HtmlSymbolType.Equals) - { - // We've found an '=' symbol, this means that the coming symbols will either be a quote - // or value (in the case that the value is unquoted). - // Spaces after/before the equal symbol are not yet supported: - // https://github.com/aspnet/Razor/issues/123 - - // TODO: Handle malformed tags, if there's an '=' then there MUST be a value. - // https://github.com/aspnet/Razor/issues/104 - - SourceLocation symbolStartLocation; - - // Check for attribute start values, aka single or double quote - if (IsQuote(htmlSymbols[i + 1])) - { - // Move past the attribute start so we can accept the true value. - i++; - symbolStartLocation = htmlSymbols[i + 1].Start; - } - else - { - symbolStartLocation = symbol.Start; - - // Set the symbol offset to 0 so we don't attempt to skip an end quote that doesn't exist. - symbolOffset = 0; - } - - attributeValueStartLocation = symbolStartLocation + - span.Start + - new SourceLocation(absoluteIndex: 1, - lineIndex: 0, - characterIndex: 1); - afterEquals = true; - } - } - - // 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; - - return CreateMarkupAttribute(name, builder, attributeValueTypes); - } - - private static KeyValuePair ParseBlock( - Block block, - IReadOnlyDictionary attributeValueTypes) - { - // 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) - { - throw new InvalidOperationException(RazorResources.TagHelpers_CannotHaveCSharpInTagDeclaration); - } - - 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 ParseSpan(childSpan, attributeValueTypes); - } - - var textSymbol = childSpan.Symbols.FirstHtmlSymbolAs(HtmlSymbolType.Text); - var name = textSymbol != null ? textSymbol.Content : null; - - if (name == null) - { - throw new InvalidOperationException(RazorResources.TagHelpers_AttributesMustHaveAName); - } - - // 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; - var endSymbol = (HtmlSymbol)endSpan.Symbols.Last(); - - // Checking to see if it's a quoted attribute, if so we should remove end quote - if (IsQuote(endSymbol)) - { - builder.Children.RemoveAt(builder.Children.Count - 1); - } - } - - // We need to rebuild the code generators of the builder and its children (this is needed to - // ensure we don't do special attribute code generation since this is a tag helper). - block = RebuildCodeGenerators(builder.Build()); - - // If there's only 1 child at this point its value could be a simple markup span (treated differently than - // block level elements for attributes). - if (block.Children.Count() == 1) - { - var child = block.Children.First() as Span; - - if (child != null) - { - // After pulling apart the block we just have a value span. - - var spanBuilder = new SpanBuilder(child); - - return CreateMarkupAttribute(name, spanBuilder, attributeValueTypes); - } - } - - return new KeyValuePair(name, block); - } - - private static Block RebuildCodeGenerators(Block block) - { - var builder = new BlockBuilder(block); - - var isDynamic = builder.CodeGenerator is DynamicAttributeBlockCodeGenerator; - - // We don't want any attribute specific logic here, null out the block code generator. - if (isDynamic || builder.CodeGenerator is AttributeBlockCodeGenerator) - { - builder.CodeGenerator = BlockCodeGenerator.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] = RebuildCodeGenerators((Block)child); - } - else - { - var childSpan = (Span)child; - ISpanCodeGenerator newCodeGenerator = null; - var literalGenerator = childSpan.CodeGenerator as LiteralAttributeCodeGenerator; - - if (literalGenerator != null) - { - if (literalGenerator.ValueGenerator == null || literalGenerator.ValueGenerator.Value == null) - { - newCodeGenerator = new MarkupCodeGenerator(); - } - else - { - newCodeGenerator = literalGenerator.ValueGenerator.Value; - } - } - else if (isDynamic && childSpan.CodeGenerator == SpanCodeGenerator.Null) - { - // Usually the dynamic code generator handles rendering the null code generators underneath - // it. This doesn't make sense in terms of tag helpers though, we need to change null code - // generators to markup code generators. - - newCodeGenerator = new MarkupCodeGenerator(); - } - - // If we have a new code generator we'll need to re-build the child - if (newCodeGenerator != null) - { - var childSpanBuilder = new SpanBuilder(childSpan) - { - CodeGenerator = newCodeGenerator - }; - - builder.Children[i] = childSpanBuilder.Build(); - } - } - } - - return builder.Build(); - } - - private static KeyValuePair CreateMarkupAttribute( - string name, - SpanBuilder builder, - IReadOnlyDictionary attributeValueTypes) - { - string attributeTypeName; - - // If the attribute was requested by the tag helper and doesn't happen to be a string then we need to treat - // its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode - // reflects that. - if (attributeValueTypes.TryGetValue(name, out attributeTypeName) && - !string.Equals(attributeTypeName, typeof(string).FullName, StringComparison.OrdinalIgnoreCase)) - { - builder.Kind = SpanKind.Code; - } - - return new KeyValuePair(name, builder.Build()); - } - - private static bool IsQuote(HtmlSymbol htmlSymbol) - { - return htmlSymbol.Type == HtmlSymbolType.DoubleQuote || - htmlSymbol.Type == HtmlSymbolType.SingleQuote; - } - - // This class is used to compare tag helper attributes by comparing only the HTML attribute name. - private class TagHelperAttributeDescriptorComparer : IEqualityComparer - { - public static readonly TagHelperAttributeDescriptorComparer Default = - new TagHelperAttributeDescriptorComparer(); - - public bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY) - { - return string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.OrdinalIgnoreCase); - } - - public int GetHashCode(TagHelperAttributeDescriptor descriptor) - { - return StringComparer.OrdinalIgnoreCase.GetHashCode(descriptor.Name); - } - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs new file mode 100644 index 0000000000..4fa31a7889 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs @@ -0,0 +1,375 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal +{ + public static class TagHelperBlockRewriter + { + public static TagHelperBlockBuilder Rewrite(string tagName, + bool validStructure, + Block tag, + IEnumerable descriptors, + ParserErrorSink 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); + + return new TagHelperBlockBuilder(tagName, start, attributes, descriptors); + } + + private static IDictionary GetTagAttributes( + string tagName, + bool validStructure, + Block tagBlock, + IEnumerable descriptors, + ParserErrorSink errorSink) + { + var attributes = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Build a dictionary so we can easily lookup expected attribute value lookups + IReadOnlyDictionary attributeValueTypes = + descriptors.SelectMany(descriptor => descriptor.Attributes) + .Distinct(TagHelperAttributeDescriptorComparer.Default) + .ToDictionary(descriptor => descriptor.Name, + descriptor => descriptor.TypeName, + StringComparer.OrdinalIgnoreCase); + + // 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) + { + KeyValuePair attribute; + bool succeeded = true; + + if (child.IsBlock) + { + succeeded = TryParseBlock(tagName, (Block)child, attributeValueTypes, errorSink, out attribute); + } + else + { + succeeded = TryParseSpan((Span)child, attributeValueTypes, errorSink, out attribute); + } + + // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span. + if (succeeded) + { + attributes[attribute.Key] = attribute.Value; + } + } + + return attributes; + } + + // 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 bool TryParseSpan( + Span span, + IReadOnlyDictionary attributeValueTypes, + ParserErrorSink errorSink, + out KeyValuePair attribute) + { + var afterEquals = false; + var builder = new SpanBuilder + { + CodeGenerator = span.CodeGenerator, + EditHandler = span.EditHandler, + Kind = span.Kind + }; + var htmlSymbols = span.Symbols.OfType().ToArray(); + var capturedAttributeValueStart = false; + var attributeValueStartLocation = span.Start; + var symbolOffset = 1; + 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) + { + // 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 && symbol.Type == HtmlSymbolType.Text) + { + name = symbol.Content; + attributeValueStartLocation = SourceLocation.Advance(span.Start, name); + } + else if (symbol.Type == HtmlSymbolType.Equals) + { + // We've found an '=' symbol, this means that the coming symbols will either be a quote + // or value (in the case that the value is unquoted). + // Spaces after/before the equal symbol are not yet supported: + // https://github.com/aspnet/Razor/issues/123 + + // TODO: Handle malformed tags, if there's an '=' then there MUST be a value. + // https://github.com/aspnet/Razor/issues/104 + + SourceLocation symbolStartLocation; + + // Check for attribute start values, aka single or double quote + if (IsQuote(htmlSymbols[i + 1])) + { + // Move past the attribute start so we can accept the true value. + i++; + symbolStartLocation = htmlSymbols[i + 1].Start; + } + else + { + symbolStartLocation = symbol.Start; + + // Set the symbol offset to 0 so we don't attempt to skip an end quote that doesn't exist. + symbolOffset = 0; + } + + attributeValueStartLocation = symbolStartLocation + + span.Start + + new SourceLocation(absoluteIndex: 1, + lineIndex: 0, + characterIndex: 1); + afterEquals = true; + } + } + + // 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) + { + errorSink.OnError(span.Start, + RazorResources.TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed, + span.Content.Length); + + attribute = default(KeyValuePair); + + return false; + } + + attribute = CreateMarkupAttribute(name, builder, attributeValueTypes); + + return true; + } + + private static bool TryParseBlock( + string tagName, + Block block, + IReadOnlyDictionary attributeValueTypes, + ParserErrorSink errorSink, + out KeyValuePair attribute) + { + // 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.Children.First().Start, + RazorResources.FormatTagHelpers_CannotHaveCSharpInTagDeclaration(tagName)); + + attribute = default(KeyValuePair); + + return false; + } + + 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, attributeValueTypes, errorSink, out attribute); + } + + var textSymbol = childSpan.Symbols.FirstHtmlSymbolAs(HtmlSymbolType.Text); + var name = textSymbol != null ? textSymbol.Content : null; + + if (name == null) + { + errorSink.OnError(childSpan.Start, RazorResources.FormatTagHelpers_AttributesMustHaveAName(tagName)); + + attribute = default(KeyValuePair); + + return false; + } + + // TODO: Support no attribute values: https://github.com/aspnet/Razor/issues/220 + + // 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; + var endSymbol = (HtmlSymbol)endSpan.Symbols.Last(); + + // Checking to see if it's a quoted attribute, if so we should remove end quote + if (IsQuote(endSymbol)) + { + builder.Children.RemoveAt(builder.Children.Count - 1); + } + } + + // We need to rebuild the code generators of the builder and its children (this is needed to + // ensure we don't do special attribute code generation since this is a tag helper). + block = RebuildCodeGenerators(builder.Build()); + + // If there's only 1 child at this point its value could be a simple markup span (treated differently than + // block level elements for attributes). + if (block.Children.Count() == 1) + { + var child = block.Children.First() as Span; + + if (child != null) + { + // After pulling apart the block we just have a value span. + + var spanBuilder = new SpanBuilder(child); + + attribute = CreateMarkupAttribute(name, spanBuilder, attributeValueTypes); + + return true; + } + } + + attribute = new KeyValuePair(name, block); + + return true; + } + + private static Block RebuildCodeGenerators(Block block) + { + var builder = new BlockBuilder(block); + + var isDynamic = builder.CodeGenerator is DynamicAttributeBlockCodeGenerator; + + // We don't want any attribute specific logic here, null out the block code generator. + if (isDynamic || builder.CodeGenerator is AttributeBlockCodeGenerator) + { + builder.CodeGenerator = BlockCodeGenerator.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] = RebuildCodeGenerators((Block)child); + } + else + { + var childSpan = (Span)child; + ISpanCodeGenerator newCodeGenerator = null; + var literalGenerator = childSpan.CodeGenerator as LiteralAttributeCodeGenerator; + + if (literalGenerator != null) + { + if (literalGenerator.ValueGenerator == null || literalGenerator.ValueGenerator.Value == null) + { + newCodeGenerator = new MarkupCodeGenerator(); + } + else + { + newCodeGenerator = literalGenerator.ValueGenerator.Value; + } + } + else if (isDynamic && childSpan.CodeGenerator == SpanCodeGenerator.Null) + { + // Usually the dynamic code generator handles rendering the null code generators underneath + // it. This doesn't make sense in terms of tag helpers though, we need to change null code + // generators to markup code generators. + + newCodeGenerator = new MarkupCodeGenerator(); + } + + // If we have a new code generator we'll need to re-build the child + if (newCodeGenerator != null) + { + var childSpanBuilder = new SpanBuilder(childSpan) + { + CodeGenerator = newCodeGenerator + }; + + builder.Children[i] = childSpanBuilder.Build(); + } + } + } + + return builder.Build(); + } + + private static KeyValuePair CreateMarkupAttribute( + string name, + SpanBuilder builder, + IReadOnlyDictionary attributeValueTypes) + { + string attributeTypeName; + + // If the attribute was requested by the tag helper and doesn't happen to be a string then we need to treat + // its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode + // reflects that. + if (attributeValueTypes.TryGetValue(name, out attributeTypeName) && + !string.Equals(attributeTypeName, typeof(string).FullName, StringComparison.OrdinalIgnoreCase)) + { + builder.Kind = SpanKind.Code; + } + + return new KeyValuePair(name, builder.Build()); + } + + private static bool IsQuote(HtmlSymbol htmlSymbol) + { + return htmlSymbol.Type == HtmlSymbolType.DoubleQuote || + htmlSymbol.Type == HtmlSymbolType.SingleQuote; + } + + // This class is used to compare tag helper attributes by comparing only the HTML attribute name. + private class TagHelperAttributeDescriptorComparer : IEqualityComparer + { + public static readonly TagHelperAttributeDescriptorComparer Default = + new TagHelperAttributeDescriptorComparer(); + + public bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY) + { + return string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(TagHelperAttributeDescriptor descriptor) + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(descriptor.Name); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs index 5a2e5b10fa..da8b97b3b9 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs @@ -27,14 +27,12 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal public void Rewrite(RewritingContext context) { - RewriteTags(context.SyntaxTree); - - ValidateRewrittenSyntaxTree(context); + RewriteTags(context.SyntaxTree, context); context.SyntaxTree = _currentBlock.Build(); } - private void RewriteTags(Block input) + private void RewriteTags(Block input, RewritingContext context) { // We want to start a new block without the children from existing (we rebuild them). TrackBlock(new BlockBuilder @@ -43,6 +41,8 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal CodeGenerator = input.CodeGenerator }); + var activeTagHelpers = _tagStack.Count; + foreach (var child in input.Children) { if (child.IsBlock) @@ -51,65 +51,18 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal if (childBlock.Type == BlockType.Tag) { - // TODO: Fully handle malformed tags: https://github.com/aspnet/Razor/issues/104 - - // Get tag name of the current block (doesn't matter if it's an end or start tag) - var tagName = GetTagName(childBlock); - - // Could not determine tag name, it can't be a TagHelper, continue on and track the element. - if (tagName == null) + if (TryRewriteTagHelper(childBlock, context)) { - _currentBlock.Children.Add(child); continue; } - if (!IsEndTag(childBlock)) - { - // We're in a begin tag block - - if (IsPotentialTagHelper(tagName, childBlock)) - { - var descriptors = _provider.GetTagHelpers(tagName); - - // We could be a tag helper, but only if we have descriptors registered - if (descriptors.Any()) - { - // Found a new tag helper block - TrackTagHelperBlock(new TagHelperBlockBuilder(tagName, descriptors, childBlock)); - - // If it's a self closing block then we don't have to worry about nested children - // within the tag... complete it. - if (IsSelfClosing(childBlock)) - { - BuildCurrentlyTrackedTagHelperBlock(); - } - - continue; - } - } - } - else - { - var currentTagHelper = _tagStack.Count > 0 ? _tagStack.Peek() : null; - - // Check if it's an "end" tag helper that matches our current tag helper - if (currentTagHelper != null && - string.Equals(currentTagHelper.TagName, tagName, StringComparison.OrdinalIgnoreCase)) - { - BuildCurrentlyTrackedTagHelperBlock(); - continue; - } - - // We're in an end tag, there won't be anymore tag helpers nested. - } - - // 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. + // 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); + RewriteTags(childBlock, context); continue; } } @@ -117,42 +70,141 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal // 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. + // 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 (activeTagHelpers != _tagStack.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(_tagStack.Count - activeTagHelpers, context); + + Debug.Assert(activeTagHelpers == _tagStack.Count); + } + BuildCurrentlyTrackedBlock(); } - private void ValidateRewrittenSyntaxTree(RewritingContext context) + private bool TryRewriteTagHelper(Block tagBlock, RewritingContext context) { - // If the blockStack still has elements in it that means there's at least one malformed TagHelper block in - // the document, that's the only way we can have a non-zero _blockStack.Count. - if (_blockStack.Count != 0) + // TODO: Fully handle malformed tags: https://github.com/aspnet/Razor/issues/104 + + // 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) { - // We reverse the children so we can search from the back to the front for the TagHelper that is - // malformed. - var candidateChildren = _currentBlock.Children.Reverse(); - var malformedTagHelper = candidateChildren.OfType().FirstOrDefault(); + return false; + } - // If the malformed tag helper is null that means something other than a TagHelper caused the - // unbalancing of the syntax tree (should never happen). - Debug.Assert(malformedTagHelper != null); + var descriptors = Enumerable.Empty(); - // We only create a single error because we can't reasonably determine other invalid tag helpers in the - // document; having one malformed tag helper puts the document into an invalid state. - context.ErrorSink.OnError( - malformedTagHelper.Start, - RazorResources.FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper( - malformedTagHelper.TagName)); + if (IsPotentialTagHelper(tagName, tagBlock)) + { + descriptors = _provider.GetTagHelpers(tagName); + } - // We need to build the remaining blocks in the stack to ensure we don't return an invalid syntax tree. - do + // If there aren't any TagHelperDescriptors registered then we aren't a TagHelper + if (!descriptors.Any()) + { + return false; + } + + if (!IsEndTag(tagBlock)) + { + // We're in a begin tag helper block + + var validTagStructure = ValidTagStructure(tagName, tagBlock, context); + + var builder = TagHelperBlockRewriter.Rewrite(tagName, + validTagStructure, + tagBlock, + descriptors, + context.ErrorSink); + + // Found a new tag helper block + TrackTagHelperBlock(builder); + + // If it's a self closing block then we don't have to worry about nested children + // within the tag... complete it. + if (IsSelfClosing(tagBlock)) { BuildCurrentlyTrackedTagHelperBlock(); } - while (_blockStack.Count != 0); } + else + { + // We're in an end tag helper block. + + var tagNameScope = _tagStack.Count > 0 ? _tagStack.Peek().TagName : string.Empty; + + // Validate that our end tag helper matches the currently scoped tag helper, if not we + // need to error. + if (tagNameScope.Equals(tagName, StringComparison.OrdinalIgnoreCase)) + { + ValidTagStructure(tagName, tagBlock, context); + + BuildCurrentlyTrackedTagHelperBlock(); + } + else + { + // Current tag helper scope does not match the end tag. Attempt to recover the tag + // helper by looking up the previous tag helper scopes for a matching tag. If we + // can't recover it means there was no corresponding tag helper begin tag. + if (TryRecoverTagHelper(tagName, context)) + { + ValidTagStructure(tagName, tagBlock, context); + + // Successfully recovered, move onto the next element. + } + else + { + // Could not recover, the end tag helper has no corresponding begin tag, create + // an error based on the current childBlock. + context.ErrorSink.OnError( + tagBlock.Start, + RazorResources.FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper(tagName)); + + return false; + } + } + } + + return true; + } + + private static bool ValidTagStructure(string tagName, Block tag, RewritingContext context) + { + // We assume an invalid structure until we verify that the tag meets all of our "valid structure" criteria. + var invalidStructure = true; + + // No need to validate the tag end because in order to be a tag block it must start with '<'. + var tagEnd = tag.Children.Last() as Span; + + // If our tag end is not a markup span it means it's some sort of code SyntaxTreeNode (not a valid format) + if (tagEnd != null && tagEnd.Kind == SpanKind.Markup) + { + var endSymbol = tagEnd.Symbols.LastOrDefault() as HtmlSymbol; + + if (endSymbol != null && endSymbol.Type == HtmlSymbolType.CloseAngle) + { + invalidStructure = false; + } + } + + if (invalidStructure) + { + context.ErrorSink.OnError( + tag.Start, + RazorResources.FormatTagHelpersParseTreeRewriter_MissingCloseAngle(tagName)); + } + + return !invalidStructure; } private void BuildCurrentlyTrackedBlock() @@ -216,6 +268,52 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal TrackBlock(builder); } + private bool TryRecoverTagHelper(string tagName, RewritingContext context) + { + var malformedTagHelperCount = 0; + + foreach (var tag in _tagStack) + { + if (tag.TagName.Equals(tagName, StringComparison.OrdinalIgnoreCase)) + { + break; + } + + malformedTagHelperCount++; + } + + // If the malformedTagHelperCount == _tagStack.Count it means we couldn't find a begin tag for the tag + // helper, can't recover. + if (malformedTagHelperCount != _tagStack.Count) + { + BuildMalformedTagHelpers(malformedTagHelperCount, context); + + // One final build, this is the build that completes our target tag helper block which is not malformed. + BuildCurrentlyTrackedTagHelperBlock(); + + // We were able to recover + return true; + } + + // Could not recover tag helper. Aka we found a tag helper end tag without a corresponding begin tag. + return false; + } + + private void BuildMalformedTagHelpers(int count, RewritingContext context) + { + for (var i = 0; i < count; i++) + { + var malformedTagHelper = _tagStack.Peek(); + + context.ErrorSink.OnError( + malformedTagHelper.Start, + RazorResources.FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper( + malformedTagHelper.TagName)); + + BuildCurrentlyTrackedTagHelperBlock(); + } + } + private static string GetTagName(Block tagBlock) { var child = tagBlock.Children.First(); @@ -240,9 +338,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal { EnsureTagBlock(beginTagBlock); - var childSpan = (Span)beginTagBlock.Children.Last(); + var childSpan = beginTagBlock.Children.Last() as Span; - return childSpan.Content.EndsWith("/>"); + return childSpan?.Content.EndsWith("/>") ?? false; } private static bool IsEndTag(Block tagBlock) diff --git a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs index 911da71cc8..fe382a5223 100644 --- a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs +++ b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs @@ -92,7 +92,7 @@ namespace Microsoft.AspNet.Razor /// /// The "@" character must be followed by a ":", "(", or a C# identifier. If you intended to switch to markup, use an HTML start tag, for example: - /// + /// /// @if(isLoggedIn) { /// <p>Hello, @user!</p> /// } @@ -104,7 +104,7 @@ namespace Microsoft.AspNet.Razor /// /// The "@" character must be followed by a ":", "(", or a C# identifier. If you intended to switch to markup, use an HTML start tag, for example: - /// + /// /// @if(isLoggedIn) { /// <p>Hello, @user!</p> /// } @@ -228,7 +228,7 @@ namespace Microsoft.AspNet.Razor /// /// Sections cannot be empty. The "@section" keyword must be followed by a block of markup surrounded by "{}". For example: - /// + /// /// @section Sidebar { /// <!-- Markup and text goes here --> /// } @@ -240,7 +240,7 @@ namespace Microsoft.AspNet.Razor /// /// Sections cannot be empty. The "@section" keyword must be followed by a block of markup surrounded by "{}". For example: - /// + /// /// @section Sidebar { /// <!-- Markup and text goes here --> /// } @@ -252,7 +252,7 @@ namespace Microsoft.AspNet.Razor /// /// Namespace imports and type aliases cannot be placed within code blocks. They must immediately follow an "@" character in markup. It is recommended that you put them at the top of the page, as in the following example: - /// + /// /// @using System.Drawing; /// @{ /// // OK here to use types from System.Drawing in the page. @@ -265,7 +265,7 @@ namespace Microsoft.AspNet.Razor /// /// Namespace imports and type aliases cannot be placed within code blocks. They must immediately follow an "@" character in markup. It is recommended that you put them at the top of the page, as in the following example: - /// + /// /// @using System.Drawing; /// @{ /// // OK here to use types from System.Drawing in the page. @@ -278,12 +278,12 @@ namespace Microsoft.AspNet.Razor /// /// Expected a "{0}" but found a "{1}". Block statements must be enclosed in "{{" and "}}". You cannot use single-statement control-flow statements in CSHTML pages. For example, the following is not allowed: - /// + /// /// @if(isLoggedIn) /// <p>Hello, @user</p> - /// + /// /// Instead, wrap the contents of the block in "{{}}": - /// + /// /// @if(isLoggedIn) {{ /// <p>Hello, @user</p> /// }} @@ -295,12 +295,12 @@ namespace Microsoft.AspNet.Razor /// /// Expected a "{0}" but found a "{1}". Block statements must be enclosed in "{{" and "}}". You cannot use single-statement control-flow statements in CSHTML pages. For example, the following is not allowed: - /// + /// /// @if(isLoggedIn) /// <p>Hello, @user</p> - /// + /// /// Instead, wrap the contents of the block in "{{}}": - /// + /// /// @if(isLoggedIn) {{ /// <p>Hello, @user</p> /// }} @@ -1431,7 +1431,7 @@ namespace Microsoft.AspNet.Razor } /// - /// Tag Helper attributes must have a name. + /// Tag Helper '{0}'s attributes must have names. /// internal static string TagHelpers_AttributesMustHaveAName { @@ -1439,15 +1439,15 @@ namespace Microsoft.AspNet.Razor } /// - /// Tag Helper attributes must have a name. + /// Tag Helper '{0}'s attributes must have names. /// - internal static string FormatTagHelpers_AttributesMustHaveAName() + internal static string FormatTagHelpers_AttributesMustHaveAName(object p0) { - return GetString("TagHelpers_AttributesMustHaveAName"); + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_AttributesMustHaveAName"), p0); } /// - /// Tag Helpers cannot have C# in an HTML tag element's attribute declaration area. + /// The tag helper '{0}' must not have C# in the element's attribute declaration area. /// internal static string TagHelpers_CannotHaveCSharpInTagDeclaration { @@ -1455,11 +1455,11 @@ namespace Microsoft.AspNet.Razor } /// - /// Tag Helpers cannot have C# in an HTML tag element's attribute declaration area. + /// The tag helper '{0}' must not have C# in the element's attribute declaration area. /// - internal static string FormatTagHelpers_CannotHaveCSharpInTagDeclaration() + internal static string FormatTagHelpers_CannotHaveCSharpInTagDeclaration(object p0) { - return GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"); + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"), p0); } /// @@ -1542,6 +1542,54 @@ namespace Microsoft.AspNet.Razor return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpersParseTreeRewriter_FoundMalformedTagHelper"), p0); } + /// + /// Missing '{0}' from '{1}' tag helper. + /// + internal static string TagHelpersParseTreeRewriter_MissingValueFromTagHelper + { + get { return GetString("TagHelpersParseTreeRewriter_MissingValueFromTagHelper"); } + } + + /// + /// Missing '{0}' from '{1}' tag helper. + /// + internal static string FormatTagHelpersParseTreeRewriter_MissingValueFromTagHelper(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpersParseTreeRewriter_MissingValueFromTagHelper"), p0, p1); + } + + /// + /// 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); + } + + /// + /// TagHelper attributes must be welformed. + /// + internal static string TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed + { + get { return GetString("TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed"); } + } + + /// + /// TagHelper attributes must be welformed. + /// + internal static string FormatTagHelperBlockRewriter_TagHelperAttributesMustBeWelformed() + { + return GetString("TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Razor/RazorResources.resx b/src/Microsoft.AspNet.Razor/RazorResources.resx index 3e25ad9efd..3f62c0eb90 100644 --- a/src/Microsoft.AspNet.Razor/RazorResources.resx +++ b/src/Microsoft.AspNet.Razor/RazorResources.resx @@ -1,17 +1,17 @@ - @@ -404,10 +404,10 @@ Instead, wrap the contents of the block in "{{}}": Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed. - Tag Helper attributes must have a name. + Tag Helper '{0}'s attributes must have names. - Tag Helpers cannot have C# in an HTML tag element's attribute declaration area. + The tag helper '{0}' must not have C# in the element's attribute declaration area. A TagHelperCodeGenerator must only be used with TagHelperBlocks. @@ -424,4 +424,13 @@ Instead, wrap the contents of the block in "{{}}": Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing. + + Missing '{0}' from '{1}' tag helper. + + + Missing close angle for tag helper '{0}'. + + + TagHelper attributes must be welformed. + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs index b78cbd1430..b43267e468 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs @@ -19,6 +19,430 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers { public class TagHelperParseTreeRewriterTest : CsHtmlMarkupParserTestBase { + public static TheoryData MalformedTagHelperAttributeBlockData + { + get + { + var factory = CreateDefaultSpanFactory(); + 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 StatementBlock( + factory.CodeTransition(), + factory.Code("do {" + extraCode).AsStatement()))); + }; + var dateTimeNow = new MarkupBlock( + new MarkupBlock( + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)))); + + return new TheoryData + { + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new Dictionary())), + new [] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + SourceLocation.Zero) + } + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new Dictionary + { + { "bar", factory.Markup("false") } + })), + new [] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + SourceLocation.Zero) + } + }, + { + "

+ { + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("strong", + new MarkupTagHelperBlock("p"))), + new [] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "strong"), + SourceLocation.Zero), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "strong"), + SourceLocation.Zero), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + absoluteIndex: 8, lineIndex: 0, columnIndex: 8) + } + }, + { + " <

", + new MarkupBlock( + blockFactory.MarkupTagBlock("<"), + blockFactory.MarkupTagBlock("<"), + blockFactory.MarkupTagBlock(""), + factory.Markup(" "), + blockFactory.MarkupTagBlock("<"), + new MarkupTagHelperBlock("p")), + new [] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "strong"), + absoluteIndex: 2, lineIndex: 0, columnIndex: 2), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + absoluteIndex: 13, lineIndex: 0, columnIndex: 13) + } + }, + { + "<<> <<>>", + 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"), + absoluteIndex: 2, lineIndex: 0, columnIndex: 2) + } + }, + { + "

", + new MarkupBlock( + blockFactory.MarkupTagBlock(""))), + new [] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + absoluteIndex: 12, lineIndex: 0, columnIndex: 12) + } + } + }; + } + } + + [Theory] + [MemberData(nameof(MalformedTagHelperBlockData))] + public void Rewrite_CreatesErrorForMalformedTagHelper( + string documentContent, + MarkupBlock expectedOutput, + RazorError[] expectedErrors) + { + RunParseTreeRewriterTest(documentContent, expectedOutput, expectedErrors, "strong", "p"); + } + public static TheoryData CodeTagHelperAttributesData { get @@ -105,9 +529,9 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers var providerContext = new TagHelperDescriptorProvider(descriptors); // Act & Assert - EvaluateData(providerContext, - documentContent, - expectedOutput, + EvaluateData(providerContext, + documentContent, + expectedOutput, expectedErrors: Enumerable.Empty()); } @@ -117,8 +541,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers { var factory = CreateDefaultSpanFactory(); var blockFactory = new BlockFactory(factory); - var errorFormat = "Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or " + - "be self closing."; + var malformedErrorFormat = "Found a malformed '{0}' tag helper. Tag helpers must have a start and " + + "end tag or be self closing."; var dateTimeNow = new MarkupBlock( new ExpressionBlock( factory.CodeTransition(), @@ -130,16 +554,23 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers "

", new MarkupBlock( new MarkupTagHelperBlock("p", - new Dictionary - { - { "class", factory.Markup("foo") }, - { "dynamic", new MarkupBlock(dateTimeNow) }, - { "style", factory.Markup("color:red;") } - }, - new MarkupTagHelperBlock("strong", - blockFactory.MarkupTagBlock("

")))), - new RazorError(string.Format(CultureInfo.InvariantCulture, errorFormat, "p"), - SourceLocation.Zero) + new Dictionary + { + { "class", factory.Markup("foo") }, + { "dynamic", new MarkupBlock(dateTimeNow) }, + { "style", factory.Markup("color:red;") } + }, + new MarkupTagHelperBlock("strong")), + blockFactory.MarkupTagBlock("")), + new RazorError[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "strong"), + absoluteIndex: 52, lineIndex: 0, columnIndex: 52), + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "strong"), + absoluteIndex: 64, lineIndex: 0, columnIndex: 64) + } }; yield return new object[] { "

Hello World

", @@ -150,8 +581,12 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers new MarkupTagHelperBlock("strong", factory.Markup("World")), blockFactory.MarkupTagBlock("
"))), - new RazorError(string.Format(CultureInfo.InvariantCulture, errorFormat, "p"), - absoluteIndex: 5, lineIndex: 0, columnIndex: 5) + new RazorError[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "p"), + absoluteIndex: 5, lineIndex: 0, columnIndex: 5) + } }; yield return new object[] { "

Hello World

", @@ -162,8 +597,15 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers new MarkupTagHelperBlock("strong", factory.Markup("World"), blockFactory.MarkupTagBlock("
")))), - new RazorError(string.Format(CultureInfo.InvariantCulture, errorFormat, "strong"), - absoluteIndex: 14, lineIndex: 0, columnIndex: 14) + new RazorError[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "p"), + absoluteIndex: 5, lineIndex: 0, columnIndex: 5), + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "strong"), + absoluteIndex: 14, lineIndex: 0, columnIndex: 14) + } }; yield return new object[] { "

Hello

World

", @@ -180,8 +622,12 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers { "style", factory.Markup("color:red;") } }, factory.Markup("World")))), - new RazorError(string.Format(CultureInfo.InvariantCulture, errorFormat, "p"), - SourceLocation.Zero) + new RazorError[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, malformedErrorFormat, "p"), + SourceLocation.Zero) + } }; } } @@ -191,9 +637,9 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers public void TagHelperParseTreeRewriter_CreatesErrorForIncompleteTagHelper( string documentContent, MarkupBlock expectedOutput, - RazorError expectedError) + RazorError[] expectedErrors) { - RunParseTreeRewriterTest(documentContent, expectedOutput, new[] { expectedError }, "strong", "p"); + RunParseTreeRewriterTest(documentContent, expectedOutput, expectedErrors, "strong", "p"); } public static TheoryData InvalidHtmlBlockData @@ -1292,8 +1738,10 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers var rewritingContext = new RewritingContext(results.Document, errorSink); new TagHelperParseTreeRewriter(provider).Rewrite(rewritingContext); var rewritten = rewritingContext.SyntaxTree; + var actualErrors = errorSink.Errors.OrderBy(error => error.Location.AbsoluteIndex) + .ToList(); - EvaluateRazorErrors(errorSink.Errors.ToList(), expectedErrors.ToList()); + EvaluateRazorErrors(actualErrors, expectedErrors.ToList()); EvaluateParseTree(rewritten, expectedOutput); }