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;
}
}
@ -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 @<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

@ -92,7 +92,7 @@ namespace Microsoft.AspNet.Razor
/// <summary>
/// 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) {
/// &lt;p&gt;Hello, @user!&lt;/p&gt;
/// }
@ -104,7 +104,7 @@ namespace Microsoft.AspNet.Razor
/// <summary>
/// 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) {
/// &lt;p&gt;Hello, @user!&lt;/p&gt;
/// }
@ -228,7 +228,7 @@ namespace Microsoft.AspNet.Razor
/// <summary>
/// Sections cannot be empty. The "@section" keyword must be followed by a block of markup surrounded by "{}". For example:
///
///
/// @section Sidebar {
/// &lt;!-- Markup and text goes here --&gt;
/// }
@ -240,7 +240,7 @@ namespace Microsoft.AspNet.Razor
/// <summary>
/// Sections cannot be empty. The "@section" keyword must be followed by a block of markup surrounded by "{}". For example:
///
///
/// @section Sidebar {
/// &lt;!-- Markup and text goes here --&gt;
/// }
@ -252,7 +252,7 @@ namespace Microsoft.AspNet.Razor
/// <summary>
/// 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
/// <summary>
/// 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
/// <summary>
/// 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)
/// &lt;p&gt;Hello, @user&lt;/p&gt;
///
///
/// Instead, wrap the contents of the block in "{{}}":
///
///
/// @if(isLoggedIn) {{
/// &lt;p&gt;Hello, @user&lt;/p&gt;
/// }}
@ -295,12 +295,12 @@ namespace Microsoft.AspNet.Razor
/// <summary>
/// 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)
/// &lt;p&gt;Hello, @user&lt;/p&gt;
///
///
/// Instead, wrap the contents of the block in "{{}}":
///
///
/// @if(isLoggedIn) {{
/// &lt;p&gt;Hello, @user&lt;/p&gt;
/// }}
@ -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

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -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
@ -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<RazorError>());
}
@ -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);
}