diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs index 6c5b51f76e..1e604c7ae2 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockBuilder.cs @@ -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 /// public SourceLocation Start { get; private set; } - private static IDictionary GetTagAttributes(Block tagBlock) + private static IDictionary GetTagAttributes( + Block tagBlock, + IEnumerable descriptors) { var attributes = new Dictionary(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 attributeValueTypes = + descriptors.SelectMany(descriptor => descriptor.Attributes) + .Distinct(TagHelperAttributeDescriptorComparer.Default) + .ToDictionary(descriptor => descriptor.Name, + descriptor => descriptor.TypeName, + StringComparer.OrdinalIgnoreCase); // We skip the first child "" or "/>". // The -2 accounts for both the start and end tags. @@ -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 ParseSpan(Span span) + private static KeyValuePair ParseSpan( + Span span, + IReadOnlyDictionary attributeValueTypes) { var afterEquals = false; var builder = new SpanBuilder @@ -192,10 +202,12 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers } } - return new KeyValuePair(name, builder.Build()); + return CreateMarkupAttribute(name, builder, attributeValueTypes); } - private static KeyValuePair ParseBlock(Block block) + private static KeyValuePair ParseBlock( + Block block, + IReadOnlyDictionary attributeValueTypes) { // TODO: Accept more than just spans: https://github.com/aspnet/Razor/issues/96. // The first child will only ever NOT be a Span if a user is doing something like: @@ -214,7 +226,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers // i.e.
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(name, block); } @@ -312,10 +340,46 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers return builder.Build(); } + private static KeyValuePair CreateMarkupAttribute( + string name, + SpanBuilder builder, + IReadOnlyDictionary attributeValueTypes) + { + string attributeTypeName; + + // If the attribute was requested by the tag helper and doesn't happen to be a string then we need to treat + // its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode + // reflects that. + if (attributeValueTypes.TryGetValue(name, out attributeTypeName) && + !string.Equals(attributeTypeName, typeof(string).FullName, StringComparison.OrdinalIgnoreCase)) + { + builder.Kind = SpanKind.Code; + } + + return new KeyValuePair(name, builder.Build()); + } + private static bool IsQuote(HtmlSymbol htmlSymbol) { return htmlSymbol.Type == HtmlSymbolType.DoubleQuote || htmlSymbol.Type == HtmlSymbolType.SingleQuote; } + + // This class is used to compare tag helper attributes by comparing only the HTML attribute name. + private class TagHelperAttributeDescriptorComparer : IEqualityComparer + { + public static readonly TagHelperAttributeDescriptorComparer Default = + new TagHelperAttributeDescriptorComparer(); + + public bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY) + { + return string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(TagHelperAttributeDescriptor descriptor) + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(descriptor.Name); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Test/Framework/TestSpanBuilder.cs b/test/Microsoft.AspNet.Razor.Test/Framework/TestSpanBuilder.cs index 79351c251e..081a9a518c 100644 --- a/test/Microsoft.AspNet.Razor.Test/Framework/TestSpanBuilder.cs +++ b/test/Microsoft.AspNet.Razor.Test/Framework/TestSpanBuilder.cs @@ -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; diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs index f6d4d52662..b78cbd1430 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs @@ -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 + { + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("person", + new Dictionary + { + { "age", factory.CodeMarkup("12") } + })) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("person", + new Dictionary + { + { "birthday", factory.CodeMarkup("DateTime.Now") } + })) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("person", + new Dictionary + { + { "name", factory.Markup("John") } + })) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("person", + new Dictionary + { + { "name", new MarkupBlock(factory.Markup("Time:"), dateTimeNow) } + })) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("person", + new Dictionary + { + { "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()); + } + public static IEnumerable IncompleteHelperBlockData { get @@ -39,9 +132,9 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers new MarkupTagHelperBlock("p", new Dictionary { - { "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("

")))), @@ -78,13 +171,13 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers new MarkupTagHelperBlock("p", new Dictionary { - { "class", new MarkupBlock(factory.Markup("foo")) } + { "class", factory.Markup("foo") } }, factory.Markup("Hello "), new MarkupTagHelperBlock("p", new Dictionary { - { "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 { - { "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 { - { "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 { - { "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 { - { "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 { - { "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 { - { "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 { - { "class", new MarkupBlock(factory.Markup("foo")) } + { "class", factory.Markup("foo") } }), factory.Markup(" "), new MarkupTagHelperBlock("p", new Dictionary { - { "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 { - { "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 { - { "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 { - { "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 { - { "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 { - { "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 { - { "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 { - { "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 { - { "class", new MarkupBlock(factory.Markup("foo")) } + { "class", factory.Markup("foo") } }, factory.Markup("Hello")), factory.Markup(" "), new MarkupTagHelperBlock("p", new Dictionary { - { "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 { - { "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(