Handle unclosed and invalid structure HTML tags for TagHelpers.

- Added detection of unclosed tags (tags without begin/end).
- Added recovery of potentially unclosed tags.
- Added detection of invalid structure tags (tags that do not end with '>').
- Modified detection of bad attribute values to be parse errors instead of runtime errors.
- Modified RazorParser to sort errors. This made writing tests more intuitive and ultimately ensures that the editor shows errors in the correct order.
- Added tests to validate invalid tag structure.
- Added tests to validate invalid attributes.
- Added tests to validate unclosed tags.

#104
This commit is contained in:
N. Taylor Mullen 2014-11-05 13:36:34 -08:00
parent a86b0dca3e
commit e30e74dc5a
6 changed files with 1139 additions and 465 deletions

View File

@ -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
{
/// <summary>
/// Instantiates a new <see cref="TagHelperBlockBuilder"/> instance based on given the
/// Instantiates a new <see cref="TagHelperBlockBuilder"/> instance based on the given
/// <paramref name="original"/>.
/// </summary>
/// <param name="original">The original <see cref="TagHelperBlock"/> to copy data from.</param>
@ -36,20 +33,21 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
/// and <see cref="BlockBuilder.Type"/> from the <paramref name="startTag"/>.
/// </summary>
/// <param name="tagName">An HTML tag name.</param>
/// <param name="start">Starting location of the <see cref="TagHelperBlock"/>.</param>
/// <param name="attributes">Attributes of the <see cref="TagHelperBlock"/>.</param>
/// <param name="descriptors">The <see cref="TagHelperDescriptor"/>s associated with the current HTML
/// tag.</param>
/// <param name="startTag">The <see cref="Block"/> that contains all information about the start
/// of the HTML element.</param>
public TagHelperBlockBuilder(string tagName, IEnumerable<TagHelperDescriptor> descriptors, Block startTag)
public TagHelperBlockBuilder(string tagName,
SourceLocation start,
IDictionary<string, SyntaxTreeNode> attributes,
IEnumerable<TagHelperDescriptor> 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<string, SyntaxTreeNode>(attributes);
}
// Internal for testing
@ -113,307 +111,5 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
/// The starting <see cref="SourceLocation"/> of the tag helper.
/// </summary>
public SourceLocation Start { get; private set; }
private static IDictionary<string, SyntaxTreeNode> GetTagAttributes(
Block tagBlock,
IEnumerable<TagHelperDescriptor> descriptors)
{
var attributes = new Dictionary<string, SyntaxTreeNode>(StringComparer.OrdinalIgnoreCase);
// Build a dictionary so we can easily lookup expected attribute value lookups
IReadOnlyDictionary<string, string> attributeValueTypes =
descriptors.SelectMany(descriptor => descriptor.Attributes)
.Distinct(TagHelperAttributeDescriptorComparer.Default)
.ToDictionary(descriptor => descriptor.Name,
descriptor => descriptor.TypeName,
StringComparer.OrdinalIgnoreCase);
// We skip the first child "<tagname" and take everything up to the "ending" portion of the tag ">" or "/>".
// The -2 accounts for both the start and end tags.
var attributeChildren = tagBlock.Children.Skip(1).Take(tagBlock.Children.Count() - 2);
foreach (var child in attributeChildren)
{
KeyValuePair<string, SyntaxTreeNode> 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<string, SyntaxTreeNode> ParseSpan(
Span span,
IReadOnlyDictionary<string, string> attributeValueTypes)
{
var afterEquals = false;
var builder = new SpanBuilder
{
CodeGenerator = span.CodeGenerator,
EditHandler = span.EditHandler,
Kind = span.Kind
};
var htmlSymbols = span.Symbols.OfType<HtmlSymbol>().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<string, SyntaxTreeNode> ParseBlock(
Block block,
IReadOnlyDictionary<string, string> 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:
// <input @checked />
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. <div class="plain text in attribute">
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<string, SyntaxTreeNode>(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<string, SyntaxTreeNode> CreateMarkupAttribute(
string name,
SpanBuilder builder,
IReadOnlyDictionary<string, string> 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<string, SyntaxTreeNode>(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<TagHelperAttributeDescriptor>
{
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);
}
}
}
}

View File

@ -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<TagHelperDescriptor> 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<string, SyntaxTreeNode> GetTagAttributes(
string tagName,
bool validStructure,
Block tagBlock,
IEnumerable<TagHelperDescriptor> descriptors,
ParserErrorSink errorSink)
{
var attributes = new Dictionary<string, SyntaxTreeNode>(StringComparer.OrdinalIgnoreCase);
// Build a dictionary so we can easily lookup expected attribute value lookups
IReadOnlyDictionary<string, string> attributeValueTypes =
descriptors.SelectMany(descriptor => descriptor.Attributes)
.Distinct(TagHelperAttributeDescriptorComparer.Default)
.ToDictionary(descriptor => descriptor.Name,
descriptor => descriptor.TypeName,
StringComparer.OrdinalIgnoreCase);
// We skip the first child "<tagname" and take everything up to the ending portion of the tag ">" or "/>".
// The -2 accounts for both the start and end tags. If the tag does not have a valid structure then there's
// no end tag to ignore.
var symbolOffset = validStructure ? 2 : 1;
var attributeChildren = tagBlock.Children.Skip(1).Take(tagBlock.Children.Count() - symbolOffset);
foreach (var child in attributeChildren)
{
KeyValuePair<string, SyntaxTreeNode> 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<string, string> attributeValueTypes,
ParserErrorSink errorSink,
out KeyValuePair<string, SyntaxTreeNode> attribute)
{
var afterEquals = false;
var builder = new SpanBuilder
{
CodeGenerator = span.CodeGenerator,
EditHandler = span.EditHandler,
Kind = span.Kind
};
var htmlSymbols = span.Symbols.OfType<HtmlSymbol>().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<string, SyntaxTreeNode>);
return false;
}
attribute = CreateMarkupAttribute(name, builder, attributeValueTypes);
return true;
}
private static bool TryParseBlock(
string tagName,
Block block,
IReadOnlyDictionary<string, string> attributeValueTypes,
ParserErrorSink errorSink,
out KeyValuePair<string, SyntaxTreeNode> 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:
// <input @checked />
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<string, SyntaxTreeNode>);
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. <div class="plain text in attribute">
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<string, SyntaxTreeNode>);
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<string, SyntaxTreeNode>(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<string, SyntaxTreeNode> CreateMarkupAttribute(
string name,
SpanBuilder builder,
IReadOnlyDictionary<string, string> 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<string, SyntaxTreeNode>(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<TagHelperAttributeDescriptor>
{
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);
}
}
}
}

View File

@ -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;
}
}
@ -121,38 +74,137 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
_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 @<p> would cause an error because there's no
// matching end </p> tag in the template block scope and therefore doesn't make sense as a tag helper.
BuildMalformedTagHelpers(_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<TagHelperBlock>().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<TagHelperDescriptor>();
// 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)

View File

@ -1431,7 +1431,7 @@ namespace Microsoft.AspNet.Razor
}
/// <summary>
/// Tag Helper attributes must have a name.
/// Tag Helper '{0}'s attributes must have names.
/// </summary>
internal static string TagHelpers_AttributesMustHaveAName
{
@ -1439,15 +1439,15 @@ namespace Microsoft.AspNet.Razor
}
/// <summary>
/// Tag Helper attributes must have a name.
/// Tag Helper '{0}'s attributes must have names.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
internal static string TagHelpers_CannotHaveCSharpInTagDeclaration
{
@ -1455,11 +1455,11 @@ namespace Microsoft.AspNet.Razor
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
@ -1542,6 +1542,54 @@ namespace Microsoft.AspNet.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpersParseTreeRewriter_FoundMalformedTagHelper"), p0);
}
/// <summary>
/// Missing '{0}' from '{1}' tag helper.
/// </summary>
internal static string TagHelpersParseTreeRewriter_MissingValueFromTagHelper
{
get { return GetString("TagHelpersParseTreeRewriter_MissingValueFromTagHelper"); }
}
/// <summary>
/// Missing '{0}' from '{1}' tag helper.
/// </summary>
internal static string FormatTagHelpersParseTreeRewriter_MissingValueFromTagHelper(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpersParseTreeRewriter_MissingValueFromTagHelper"), p0, p1);
}
/// <summary>
/// Missing close angle for tag helper '{0}'.
/// </summary>
internal static string TagHelpersParseTreeRewriter_MissingCloseAngle
{
get { return GetString("TagHelpersParseTreeRewriter_MissingCloseAngle"); }
}
/// <summary>
/// Missing close angle for tag helper '{0}'.
/// </summary>
internal static string FormatTagHelpersParseTreeRewriter_MissingCloseAngle(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpersParseTreeRewriter_MissingCloseAngle"), p0);
}
/// <summary>
/// TagHelper attributes must be welformed.
/// </summary>
internal static string TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed
{
get { return GetString("TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed"); }
}
/// <summary>
/// TagHelper attributes must be welformed.
/// </summary>
internal static string FormatTagHelperBlockRewriter_TagHelperAttributesMustBeWelformed()
{
return GetString("TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed");
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -404,10 +404,10 @@ Instead, wrap the contents of the block in "{{}}":
<value>Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed.</value>
</data>
<data name="TagHelpers_AttributesMustHaveAName" xml:space="preserve">
<value>Tag Helper attributes must have a name.</value>
<value>Tag Helper '{0}'s attributes must have names.</value>
</data>
<data name="TagHelpers_CannotHaveCSharpInTagDeclaration" xml:space="preserve">
<value>Tag Helpers cannot have C# in an HTML tag element's attribute declaration area.</value>
<value>The tag helper '{0}' must not have C# in the element's attribute declaration area.</value>
</data>
<data name="TagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock" xml:space="preserve">
<value>A TagHelperCodeGenerator must only be used with TagHelperBlocks.</value>
@ -424,4 +424,13 @@ Instead, wrap the contents of the block in "{{}}":
<data name="TagHelpersParseTreeRewriter_FoundMalformedTagHelper" xml:space="preserve">
<value>Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing.</value>
</data>
<data name="TagHelpersParseTreeRewriter_MissingValueFromTagHelper" xml:space="preserve">
<value>Missing '{0}' from '{1}' tag helper.</value>
</data>
<data name="TagHelpersParseTreeRewriter_MissingCloseAngle" xml:space="preserve">
<value>Missing close angle for tag helper '{0}'.</value>
</data>
<data name="TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed" xml:space="preserve">
<value>TagHelper attributes must be welformed.</value>
</data>
</root>

View File

@ -19,6 +19,430 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
{
public class TagHelperParseTreeRewriterTest : CsHtmlMarkupParserTestBase
{
public static TheoryData<string, MarkupBlock, RazorError[]> 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<string, Block> 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<string, MarkupBlock, RazorError[]>
{
{
"<p =\"false\"\" ></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>())),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero)
}
},
{
"<p bar=\"false\"\" <strong>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "bar", factory.Markup("false") }
})),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero)
}
},
{
"<p bar='false <strong>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "bar", new MarkupBlock(factory.Markup("false"), factory.Markup(" <strong>")) }
})),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero)
}
},
{
"<p foo bar<strong>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("strong"))),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "strong"),
absoluteIndex: 10, lineIndex: 0, columnIndex: 10)
}
},
{
"<p class=btn\" bar<strong>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", factory.Markup("btn") }
})),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero)
}
},
{
"<p class=btn\" bar=\"foo\"<strong>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", factory.Markup("btn") }
})),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero)
}
},
{
"<p class=\"btn bar=\"foo\"<strong>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("btn"), factory.Markup(" bar=")) },
{ "foo", factory.Markup(string.Empty) }
},
new MarkupTagHelperBlock("strong"))),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "strong"),
absoluteIndex: 23, lineIndex: 0, columnIndex: 23)
}
},
{
"<p class=\"btn bar=\"foo\"></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("btn"), factory.Markup(" bar=")) },
})),
new RazorError[0]
},
{
"<p @DateTime.Now class=\"btn\"></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p")),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCSharp, "p"),
absoluteIndex: 3, lineIndex: 0 , columnIndex: 3)
}
},
{
"<p @DateTime.Now=\"btn\"></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p")),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCSharp, "p"),
absoluteIndex: 3, lineIndex: 0 , columnIndex: 3)
}
},
{
"<p class=@DateTime.Now\"></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", dateTimeNow }
})),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero)
}
},
{
"<p class=\"@do {",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", createInvalidDoBlock(string.Empty) }
})),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero),
new RazorError(
RazorResources.FormatParseError_Expected_EndOfBlock_Before_EOF("do", "}", "{"),
absoluteIndex: 11, lineIndex: 0, columnIndex: 11)
}
},
{
"<p class=\"@do {\"></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", createInvalidDoBlock("\"></p>") }
})),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero),
new RazorError(
RazorResources.FormatParseError_Expected_EndOfBlock_Before_EOF("do", "}", "{"),
absoluteIndex: 11, lineIndex: 0, columnIndex: 11),
new RazorError(
RazorResources.ParseError_Unterminated_String_Literal,
absoluteIndex: 15, lineIndex: 0, columnIndex: 15)
}
},
{
"<p @do { someattribute=\"btn\"></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p")),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCSharp, "p"),
absoluteIndex: 3, lineIndex: 0 , columnIndex: 3),
new RazorError(
RazorResources.FormatParseError_Expected_EndOfBlock_Before_EOF("do", "}", "{"),
absoluteIndex: 4, lineIndex: 0, columnIndex: 4),
new RazorError(
RazorResources.FormatParseError_UnexpectedEndTag("p"),
absoluteIndex: 29, lineIndex: 0, columnIndex: 29)
}
}
};
}
}
[Theory]
[MemberData(nameof(MalformedTagHelperAttributeBlockData))]
public void Rewrite_CreatesErrorForMalformedTagHelpersWithAttributes(
string documentContent,
MarkupBlock expectedOutput,
RazorError[] expectedErrors)
{
RunParseTreeRewriterTest(documentContent, expectedOutput, expectedErrors, "strong", "p");
}
public static TheoryData<string, MarkupBlock, RazorError[]> MalformedTagHelperBlockData
{
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}'.";
return new TheoryData<string, MarkupBlock, RazorError[]>
{
{
"<p",
new MarkupBlock(
new MarkupTagHelperBlock("p")),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero)
}
},
{
"<p></p",
new MarkupBlock(
new MarkupTagHelperBlock("p")),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
absoluteIndex: 3, lineIndex: 0, columnIndex: 3)
}
},
{
"<p><strong",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new MarkupTagHelperBlock("strong"))),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "strong"),
absoluteIndex: 3, lineIndex: 0, columnIndex: 3),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "strong"),
absoluteIndex: 3, lineIndex: 0, columnIndex: 3)
}
},
{
"<strong <p>",
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)
}
},
{
"<strong </strong",
new MarkupBlock(
new MarkupTagHelperBlock("strong")),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "strong"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "strong"),
absoluteIndex: 8, lineIndex: 0, columnIndex: 8)
}
},
{
"<<</strong> <<p>",
new MarkupBlock(
blockFactory.MarkupTagBlock("<"),
blockFactory.MarkupTagBlock("<"),
blockFactory.MarkupTagBlock("</strong>"),
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)
}
},
{
"<<<strong>> <<>>",
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)
}
},
{
"<str<strong></p></strong>",
new MarkupBlock(
blockFactory.MarkupTagBlock("<str"),
new MarkupTagHelperBlock("strong",
blockFactory.MarkupTagBlock("</p>"))),
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
@ -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
"<p class=foo dynamic=@DateTime.Now style=color:red;><strong></p></strong>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", factory.Markup("foo") },
{ "dynamic", new MarkupBlock(dateTimeNow) },
{ "style", factory.Markup("color:red;") }
},
new MarkupTagHelperBlock("strong",
blockFactory.MarkupTagBlock("</p>")))),
new RazorError(string.Format(CultureInfo.InvariantCulture, errorFormat, "p"),
SourceLocation.Zero)
new Dictionary<string, SyntaxTreeNode>
{
{ "class", factory.Markup("foo") },
{ "dynamic", new MarkupBlock(dateTimeNow) },
{ "style", factory.Markup("color:red;") }
},
new MarkupTagHelperBlock("strong")),
blockFactory.MarkupTagBlock("</strong>")),
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[] {
"<div><p>Hello <strong>World</strong></div>",
@ -150,8 +581,12 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("strong",
factory.Markup("World")),
blockFactory.MarkupTagBlock("</div>"))),
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[] {
"<div><p>Hello <strong>World</div>",
@ -162,8 +597,15 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("strong",
factory.Markup("World"),
blockFactory.MarkupTagBlock("</div>")))),
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[] {
"<p class=\"foo\">Hello <p style=\"color:red;\">World</p>",
@ -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<string, MarkupBlock> 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);
}