Changed TagHelper attributes to be SpanKind.Code if not string typed.

- Also added tests to validate that non string TagHelper attributes inherit the SpanKind.COde behavior.
- Removed a block wrapping around single markup spans.
This commit is contained in:
NTaylorMullen 2014-11-19 17:44:24 -08:00 committed by N. Taylor Mullen
parent 0d60da296d
commit c947e9ffaa
3 changed files with 203 additions and 41 deletions

View File

@ -44,7 +44,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
TagName = tagName;
CodeGenerator = new TagHelperCodeGenerator(descriptors);
Type = startTag.Type;
Attributes = GetTagAttributes(startTag);
Attributes = GetTagAttributes(startTag, descriptors);
// There will always be at least one child for the '<'.
Start = startTag.Children.First().Start;
@ -107,11 +107,19 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
/// </summary>
public SourceLocation Start { get; private set; }
private static IDictionary<string, SyntaxTreeNode> GetTagAttributes(Block tagBlock)
private static IDictionary<string, SyntaxTreeNode> GetTagAttributes(
Block tagBlock,
IEnumerable<TagHelperDescriptor> descriptors)
{
var attributes = new Dictionary<string, SyntaxTreeNode>(StringComparer.OrdinalIgnoreCase);
// TODO: Handle malformed tags: https://github.com/aspnet/razor/issues/104
// 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.
@ -123,11 +131,11 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
if (child.IsBlock)
{
attribute = ParseBlock((Block)child);
attribute = ParseBlock((Block)child, attributeValueTypes);
}
else
{
attribute = ParseSpan((Span)child);
attribute = ParseSpan((Span)child, attributeValueTypes);
}
attributes.Add(attribute.Key, attribute.Value);
@ -139,7 +147,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
// 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)
private static KeyValuePair<string, SyntaxTreeNode> ParseSpan(
Span span,
IReadOnlyDictionary<string, string> attributeValueTypes)
{
var afterEquals = false;
var builder = new SpanBuilder
@ -192,10 +202,12 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
}
}
return new KeyValuePair<string, SyntaxTreeNode>(name, builder.Build());
return CreateMarkupAttribute(name, builder, attributeValueTypes);
}
private static KeyValuePair<string, SyntaxTreeNode> ParseBlock(Block block)
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:
@ -214,7 +226,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
// i.e. <div class="plain text in attribute">
if (builder.Children.Count == 1)
{
return ParseSpan(childSpan);
return ParseSpan(childSpan, attributeValueTypes);
}
var textSymbol = childSpan.Symbols.FirstHtmlSymbolAs(HtmlSymbolType.Text);
@ -246,6 +258,22 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
// 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);
}
@ -312,10 +340,46 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
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

@ -114,6 +114,11 @@ namespace Microsoft.AspNet.Razor.Test.Framework
return self.Span(SpanKind.Markup, content, markup: true).With(new MarkupCodeGenerator());
}
public static SpanConstructor CodeMarkup(this SpanFactory self, params string[] content)
{
return self.Span(SpanKind.Code, content, markup: true).With(new MarkupCodeGenerator());
}
public static SourceLocation GetLocationAndAdvance(this SourceLocationTracker self, string content)
{
var ret = self.CurrentLocation;

View File

@ -1,6 +1,7 @@
// 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.Globalization;
using System.Linq;
@ -18,6 +19,98 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
{
public class TagHelperParseTreeRewriterTest : CsHtmlMarkupParserTestBase
{
public static TheoryData CodeTagHelperAttributesData
{
get
{
var factory = CreateDefaultSpanFactory();
var dateTimeNow = new MarkupBlock(
factory.Markup(" "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("DateTime.Now")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharacters.NonWhiteSpace)));
return new TheoryData<string, Block>
{
{
"<person age=\"12\" />",
new MarkupBlock(
new MarkupTagHelperBlock("person",
new Dictionary<string, SyntaxTreeNode>
{
{ "age", factory.CodeMarkup("12") }
}))
},
{
"<person birthday=\"DateTime.Now\" />",
new MarkupBlock(
new MarkupTagHelperBlock("person",
new Dictionary<string, SyntaxTreeNode>
{
{ "birthday", factory.CodeMarkup("DateTime.Now") }
}))
},
{
"<person name=\"John\" />",
new MarkupBlock(
new MarkupTagHelperBlock("person",
new Dictionary<string, SyntaxTreeNode>
{
{ "name", factory.Markup("John") }
}))
},
{
"<person name=\"Time: @DateTime.Now\" />",
new MarkupBlock(
new MarkupTagHelperBlock("person",
new Dictionary<string, SyntaxTreeNode>
{
{ "name", new MarkupBlock(factory.Markup("Time:"), dateTimeNow) }
}))
},
{
"<person age=\"12\" birthday=\"DateTime.Now\" name=\"Time: @DateTime.Now\" />",
new MarkupBlock(
new MarkupTagHelperBlock("person",
new Dictionary<string, SyntaxTreeNode>
{
{ "age", factory.CodeMarkup("12") },
{ "birthday", factory.CodeMarkup("DateTime.Now") },
{ "name", new MarkupBlock(factory.Markup("Time:"), dateTimeNow) }
}))
},
};
}
}
[Theory]
[MemberData(nameof(CodeTagHelperAttributesData))]
public void TagHelperParseTreeRewriter_CreatesMarkupCodeSpansForNonStringTagHelperAttributes(
string documentContent,
MarkupBlock expectedOutput)
{
// Arrange
var descriptors = new TagHelperDescriptor[]
{
new TagHelperDescriptor("person", "PersonTagHelper", "personAssembly", ContentBehavior.None,
attributes: new[]
{
new TagHelperAttributeDescriptor("age", "Age", typeof(int).FullName),
new TagHelperAttributeDescriptor("birthday", "BirthDay", typeof(DateTime).FullName),
new TagHelperAttributeDescriptor("name", "Name", typeof(string).FullName),
})
};
var providerContext = new TagHelperDescriptorProvider(descriptors);
// Act & Assert
EvaluateData(providerContext,
documentContent,
expectedOutput,
expectedErrors: Enumerable.Empty<RazorError>());
}
public static IEnumerable<object[]> IncompleteHelperBlockData
{
get
@ -39,9 +132,9 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "class", factory.Markup("foo") },
{ "dynamic", new MarkupBlock(dateTimeNow) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "style", factory.Markup("color:red;") }
},
new MarkupTagHelperBlock("strong",
blockFactory.MarkupTagBlock("</p>")))),
@ -78,13 +171,13 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) }
{ "class", factory.Markup("foo") }
},
factory.Markup("Hello "),
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "style", factory.Markup("color:red;") }
},
factory.Markup("World")))),
new RazorError(string.Format(CultureInfo.InvariantCulture, errorFormat, "p"),
@ -426,7 +519,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup(" foo")) },
{ "class", factory.Markup(" foo") },
{ "style",
new MarkupBlock(
factory.Markup(" color"),
@ -443,7 +536,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup(" foo")) },
{ "class", factory.Markup(" foo") },
{ "style",
new MarkupBlock(
factory.Markup(" color"),
@ -782,8 +875,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("script",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "class", factory.Markup("foo") },
{ "style", factory.Markup("color:red;") }
}))
};
yield return new object[] {
@ -794,8 +887,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("script",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "class", factory.Markup("foo") },
{ "style", factory.Markup("color:red;") }
}),
factory.Markup(" World")))
};
@ -823,8 +916,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "class", factory.Markup("foo") },
{ "style", factory.Markup("color:red;") }
}))
};
yield return new object[] {
@ -835,8 +928,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "class", factory.Markup("foo") },
{ "style", factory.Markup("color:red;") }
}),
factory.Markup(" World")))
};
@ -847,13 +940,13 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) }
{ "class", factory.Markup("foo") }
}),
factory.Markup(" "),
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "style", factory.Markup("color:red;") }
}),
factory.Markup("World"))
};
@ -888,9 +981,9 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "class", factory.Markup("foo") },
{ "dynamic", new MarkupBlock(dateTimeNow) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "style", factory.Markup("color:red;") }
}))
};
yield return new object[] {
@ -899,9 +992,9 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "class", factory.Markup("foo") },
{ "dynamic", new MarkupBlock(dateTimeNow) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "style", factory.Markup("color:red;") }
},
factory.Markup("Hello World")))
};
@ -911,7 +1004,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "class", factory.Markup("foo") },
{ "dynamic", new MarkupBlock(dateTimeNow) }
},
factory.Markup("Hello")),
@ -919,7 +1012,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "style", new MarkupBlock(factory.Markup("color:red;")) },
{ "style", factory.Markup("color:red;") },
{ "dynamic", new MarkupBlock(dateTimeNow) }
},
factory.Markup("World")))
@ -930,9 +1023,9 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "class", factory.Markup("foo") },
{ "dynamic", new MarkupBlock(dateTimeNow) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "style", factory.Markup("color:red;") }
},
factory.Markup("Hello World "),
new MarkupTagBlock(
@ -973,8 +1066,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "class", factory.Markup("foo") },
{ "style", factory.Markup("color:red;") }
}))
};
yield return new object[] {
@ -983,8 +1076,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "class", factory.Markup("foo") },
{ "style", factory.Markup("color:red;") }
},
factory.Markup("Hello World")))
};
@ -994,14 +1087,14 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) }
{ "class", factory.Markup("foo") }
},
factory.Markup("Hello")),
factory.Markup(" "),
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "style", factory.Markup("color:red;") }
},
factory.Markup("World")))
};
@ -1011,8 +1104,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
{ "class", factory.Markup("foo") },
{ "style", factory.Markup("color:red;") }
},
factory.Markup("Hello World "),
new MarkupTagBlock(