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:
parent
a86b0dca3e
commit
e30e74dc5a
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
/// <p>Hello, @user!</p>
|
||||
/// }
|
||||
|
|
@ -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) {
|
||||
/// <p>Hello, @user!</p>
|
||||
/// }
|
||||
|
|
@ -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 {
|
||||
/// <!-- Markup and text goes here -->
|
||||
/// }
|
||||
|
|
@ -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 {
|
||||
/// <!-- Markup and text goes here -->
|
||||
/// }
|
||||
|
|
@ -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)
|
||||
/// <p>Hello, @user</p>
|
||||
///
|
||||
///
|
||||
/// Instead, wrap the contents of the block in "{{}}":
|
||||
///
|
||||
///
|
||||
/// @if(isLoggedIn) {{
|
||||
/// <p>Hello, @user</p>
|
||||
/// }}
|
||||
|
|
@ -295,12 +295,12 @@ namespace Microsoft.AspNet.Razor
|
|||
|
||||
/// <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)
|
||||
/// <p>Hello, @user</p>
|
||||
///
|
||||
///
|
||||
/// Instead, wrap the contents of the block in "{{}}":
|
||||
///
|
||||
///
|
||||
/// @if(isLoggedIn) {{
|
||||
/// <p>Hello, @user</p>
|
||||
/// }}
|
||||
|
|
@ -1431,7 +1431,7 @@ namespace Microsoft.AspNet.Razor
|
|||
}
|
||||
|
||||
/// <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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue