diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs index 4fa31a788..5f8512a14 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs @@ -93,7 +93,10 @@ private static bool TryParseSpan( var htmlSymbols = span.Symbols.OfType().ToArray(); var capturedAttributeValueStart = false; var attributeValueStartLocation = span.Start; - var symbolOffset = 1; + + // The symbolOffset is initialized to 0 to expect worst case: "class=". If a quote is found later on for + // the attribute value the symbolOffset is adjusted accordingly. + var symbolOffset = 0; string name = null; // Iterate down through the symbols to find the name and the start of the value. @@ -119,7 +122,7 @@ private static bool TryParseSpan( else if (name == null && symbol.Type == HtmlSymbolType.Text) { name = symbol.Content; - attributeValueStartLocation = SourceLocation.Advance(span.Start, name); + attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, name); } else if (symbol.Type == HtmlSymbolType.Equals) { @@ -134,11 +137,14 @@ private static bool TryParseSpan( SourceLocation symbolStartLocation; // Check for attribute start values, aka single or double quote - if (IsQuote(htmlSymbols[i + 1])) + if (i + 1 < htmlSymbols.Length && IsQuote(htmlSymbols[i + 1])) { // Move past the attribute start so we can accept the true value. i++; - symbolStartLocation = htmlSymbols[i + 1].Start; + symbolStartLocation = htmlSymbols[i].Start; + + // If there's a start quote then there must be an end quote to be valid, skip it. + symbolOffset = 1; } else { @@ -148,13 +154,17 @@ private static bool TryParseSpan( symbolOffset = 0; } - attributeValueStartLocation = symbolStartLocation + - span.Start + - new SourceLocation(absoluteIndex: 1, - lineIndex: 0, - characterIndex: 1); + attributeValueStartLocation = + span.Start + + symbolStartLocation + + new SourceLocation(absoluteIndex: 1, lineIndex: 0, characterIndex: 1); + afterEquals = true; } + else if (symbol.Type == HtmlSymbolType.WhiteSpace) + { + attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, symbol.Content); + } } // After all symbols have been added we need to set the builders start position so we do not indirectly diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs index a706e68e5..75128bc2b 100644 --- a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs @@ -213,7 +213,41 @@ public static TheoryData DesignTimeTagHelperTestData BuildLineMapping(1094, 30, 18, 5179, 187, 19, 29), BuildLineMapping(1231, 34, 5279, 192, 0, 1), } - } + }, + { + "EmptyAttributeTagHelpers", + "EmptyAttributeTagHelpers.DesignTime", + PAndInputTagHelperDescriptors, + new List + { + BuildLineMapping(documentAbsoluteIndex: 14, + documentLineIndex: 0, + generatedAbsoluteIndex: 493, + generatedLineIndex: 15, + characterOffsetIndex: 14, + contentLength: 11), + BuildLineMapping(documentAbsoluteIndex: 62, + documentLineIndex: 3, + documentCharacterOffsetIndex: 26, + generatedAbsoluteIndex: 1289, + generatedLineIndex: 39, + generatedCharacterOffsetIndex: 28, + contentLength: 0), + BuildLineMapping(documentAbsoluteIndex: 122, + documentLineIndex: 5, + generatedAbsoluteIndex: 1634, + generatedLineIndex: 48, + characterOffsetIndex: 30, + contentLength: 0), + BuildLineMapping(documentAbsoluteIndex: 88, + documentLineIndex: 4, + documentCharacterOffsetIndex: 12, + generatedAbsoluteIndex: 1789, + generatedLineIndex: 54, + generatedCharacterOffsetIndex: 19, + contentLength: 0) + } + }, }; } } @@ -245,6 +279,7 @@ public static TheoryData RuntimeTimeTagHelperTestData { "BasicTagHelpers", PAndInputTagHelperDescriptors }, { "BasicTagHelpers.RemoveTagHelper", PAndInputTagHelperDescriptors }, { "ComplexTagHelpers", PAndInputTagHelperDescriptors }, + { "EmptyAttributeTagHelpers", PAndInputTagHelperDescriptors }, }; } } diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs index 2425da44e..68d0c1746 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs @@ -19,6 +19,77 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers { public class TagHelperParseTreeRewriterTest : CsHtmlMarkupParserTestBase { + public static TheoryData EmptyAttributeTagHelperData + { + get + { + var factory = CreateDefaultSpanFactory(); + + // documentContent, expectedOutput + return new TheoryData + { + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new Dictionary + { + { "class", new MarkupBlock() } + })) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new Dictionary + { + { "class", new MarkupBlock() } + })) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new Dictionary + { + // We expected a markup node here because attribute values without quotes can only ever + // be a single item, hence don't need to be enclosed by a block. + { "class", factory.Markup("").With(SpanCodeGenerator.Null) }, + })) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new Dictionary + { + { "class1", new MarkupBlock() }, + { "class2", factory.Markup("").With(SpanCodeGenerator.Null) }, + { "class3", new MarkupBlock() }, + })) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new Dictionary + { + { "class1", new MarkupBlock() }, + { "class2", new MarkupBlock() }, + { "class3", factory.Markup("").With(SpanCodeGenerator.Null) }, + })) + }, + }; + } + } + + [Theory] + [MemberData(nameof(EmptyAttributeTagHelperData))] + public void Rewrite_UnderstandsEmptyAttributeTagHelpers(string documentContent, MarkupBlock expectedOutput) + { + RunParseTreeRewriterTest(documentContent, expectedOutput, new RazorError[0], "p"); + } + public static TheoryData MalformedTagHelperAttributeBlockData { get diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyAttributeTagHelpers.DesignTime.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyAttributeTagHelpers.DesignTime.cs new file mode 100644 index 000000000..931181397 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyAttributeTagHelpers.DesignTime.cs @@ -0,0 +1,62 @@ +namespace TestOutput +{ + using Microsoft.AspNet.Razor.Runtime.TagHelpers; + using System; + using System.Threading.Tasks; + + public class EmptyAttributeTagHelpers + { + private static object @__o; + private void @__RazorDesignTimeHelpers__() + { + #pragma warning disable 219 + string __tagHelperDirectiveSyntaxHelper = null; + __tagHelperDirectiveSyntaxHelper = +#line 1 "EmptyAttributeTagHelpers.cshtml" + "something" + +#line default +#line hidden + ; + #pragma warning restore 219 + } + #line hidden + private InputTagHelper __InputTagHelper = null; + private InputTagHelper2 __InputTagHelper2 = null; + private PTagHelper __PTagHelper = null; + #line hidden + public EmptyAttributeTagHelpers() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + __InputTagHelper = CreateTagHelper(); + __InputTagHelper.Type = ""; + __InputTagHelper2 = CreateTagHelper(); + __InputTagHelper2.Type = __InputTagHelper.Type; +#line 4 "EmptyAttributeTagHelpers.cshtml" +__InputTagHelper2.Checked = ; + +#line default +#line hidden + __InputTagHelper = CreateTagHelper(); + __InputTagHelper.Type = ""; + __InputTagHelper2 = CreateTagHelper(); + __InputTagHelper2.Type = __InputTagHelper.Type; +#line 6 "EmptyAttributeTagHelpers.cshtml" + __InputTagHelper2.Checked = ; + +#line default +#line hidden + __PTagHelper = CreateTagHelper(); +#line 5 "EmptyAttributeTagHelpers.cshtml" +__PTagHelper.Age = ; + +#line default +#line hidden + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyAttributeTagHelpers.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyAttributeTagHelpers.cs new file mode 100644 index 000000000..aba79edb5 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyAttributeTagHelpers.cs @@ -0,0 +1,141 @@ +#pragma checksum "EmptyAttributeTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "828f744feb52d497814b7a018f817f31d92085ce" +namespace TestOutput +{ + using Microsoft.AspNet.Razor.Runtime.TagHelpers; + using System; + using System.Threading.Tasks; + + public class EmptyAttributeTagHelpers + { + #line hidden + #pragma warning disable 0414 + private System.IO.TextWriter __tagHelperStringValueBuffer = null; + #pragma warning restore 0414 + private TagHelperExecutionContext __tagHelperExecutionContext = null; + private TagHelperRunner __tagHelperRunner = new TagHelperRunner(); + private TagHelperScopeManager __tagHelperScopeManager = new TagHelperScopeManager(); + private InputTagHelper __InputTagHelper = null; + private InputTagHelper2 __InputTagHelper2 = null; + private PTagHelper __PTagHelper = null; + #line hidden + public EmptyAttributeTagHelpers() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + Instrumentation.BeginContext(27, 13, true); + WriteLiteral("\r\n

\r\n "); + Instrumentation.EndContext(); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", "test", async() => { + } + , StartWritingScope, EndWritingScope); + __InputTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__InputTagHelper); + __InputTagHelper.Type = ""; + __tagHelperExecutionContext.AddTagHelperAttribute("type", __InputTagHelper.Type); + __InputTagHelper2 = CreateTagHelper(); + __tagHelperExecutionContext.Add(__InputTagHelper2); + __InputTagHelper2.Type = __InputTagHelper.Type; +#line 4 "EmptyAttributeTagHelpers.cshtml" +__InputTagHelper2.Checked = ; + +#line default +#line hidden + __tagHelperExecutionContext.AddTagHelperAttribute("checked", __InputTagHelper2.Checked); + __tagHelperExecutionContext.AddHtmlAttribute("class", ""); + __tagHelperExecutionContext.Output = __tagHelperRunner.RunAsync(__tagHelperExecutionContext).Result; + WriteLiteral(__tagHelperExecutionContext.Output.GenerateStartTag()); + WriteLiteral(__tagHelperExecutionContext.Output.GeneratePreContent()); + if (__tagHelperExecutionContext.Output.ContentSet) + { + WriteLiteral(__tagHelperExecutionContext.Output.GenerateContent()); + } + else if (__tagHelperExecutionContext.ChildContentRetrieved) + { + WriteLiteral(__tagHelperExecutionContext.GetChildContentAsync().Result); + } + else + { + __tagHelperExecutionContext.ExecuteChildContentAsync().Wait(); + } + WriteLiteral(__tagHelperExecutionContext.Output.GeneratePostContent()); + WriteLiteral(__tagHelperExecutionContext.Output.GenerateEndTag()); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + Instrumentation.BeginContext(74, 6, true); + WriteLiteral("\r\n "); + Instrumentation.EndContext(); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("p", "test", async() => { + WriteLiteral("\r\n "); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", "test", async() => { + } + , StartWritingScope, EndWritingScope); + __InputTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__InputTagHelper); + __InputTagHelper.Type = ""; + __tagHelperExecutionContext.AddTagHelperAttribute("type", __InputTagHelper.Type); + __InputTagHelper2 = CreateTagHelper(); + __tagHelperExecutionContext.Add(__InputTagHelper2); + __InputTagHelper2.Type = __InputTagHelper.Type; +#line 6 "EmptyAttributeTagHelpers.cshtml" + __InputTagHelper2.Checked = ; + +#line default +#line hidden + __tagHelperExecutionContext.AddTagHelperAttribute("checked", __InputTagHelper2.Checked); + __tagHelperExecutionContext.AddHtmlAttribute("class", ""); + __tagHelperExecutionContext.Output = __tagHelperRunner.RunAsync(__tagHelperExecutionContext).Result; + WriteLiteral(__tagHelperExecutionContext.Output.GenerateStartTag()); + WriteLiteral(__tagHelperExecutionContext.Output.GeneratePreContent()); + if (__tagHelperExecutionContext.Output.ContentSet) + { + WriteLiteral(__tagHelperExecutionContext.Output.GenerateContent()); + } + else if (__tagHelperExecutionContext.ChildContentRetrieved) + { + WriteLiteral(__tagHelperExecutionContext.GetChildContentAsync().Result); + } + else + { + __tagHelperExecutionContext.ExecuteChildContentAsync().Wait(); + } + WriteLiteral(__tagHelperExecutionContext.Output.GeneratePostContent()); + WriteLiteral(__tagHelperExecutionContext.Output.GenerateEndTag()); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + WriteLiteral("\r\n "); + } + , StartWritingScope, EndWritingScope); + __PTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__PTagHelper); +#line 5 "EmptyAttributeTagHelpers.cshtml" +__PTagHelper.Age = ; + +#line default +#line hidden + __tagHelperExecutionContext.AddTagHelperAttribute("age", __PTagHelper.Age); + __tagHelperExecutionContext.Output = __tagHelperRunner.RunAsync(__tagHelperExecutionContext).Result; + WriteLiteral(__tagHelperExecutionContext.Output.GenerateStartTag()); + WriteLiteral(__tagHelperExecutionContext.Output.GeneratePreContent()); + if (__tagHelperExecutionContext.Output.ContentSet) + { + WriteLiteral(__tagHelperExecutionContext.Output.GenerateContent()); + } + else if (__tagHelperExecutionContext.ChildContentRetrieved) + { + WriteLiteral(__tagHelperExecutionContext.GetChildContentAsync().Result); + } + else + { + __tagHelperExecutionContext.ExecuteChildContentAsync().Wait(); + } + WriteLiteral(__tagHelperExecutionContext.Output.GeneratePostContent()); + WriteLiteral(__tagHelperExecutionContext.Output.GenerateEndTag()); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + Instrumentation.BeginContext(144, 8, true); + WriteLiteral("\r\n
"); + Instrumentation.EndContext(); + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyAttributeTagHelpers.cshtml b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyAttributeTagHelpers.cshtml new file mode 100644 index 000000000..9a294f796 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyAttributeTagHelpers.cshtml @@ -0,0 +1,8 @@ +@addtaghelper "something" + +
+ +

+ +

+
\ No newline at end of file