From b25bf01158f69bce1db4c8e2b244bc8c21403273 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Sun, 17 May 2015 17:07:51 -0700 Subject: [PATCH] Add TagHelper support for unbound data- attributes. - Involved updating the HtmlMarkupParser to properly separate data- attributes. Prior to this change `data-foo="abc @DateTime.Now def"` would involve 1 Span for `data-foo="abc` 1 Span for `@DateTime.Now` and 1 Span for `def"`. This was very unique behavior from an attribute standpoint (as far as Razor is concerned) and made it difficult for the TagHelper rewriting system to rewrite attributes. With this change it gets broken out as follows: `|data-foo="|abc| @DateTime.Now| def|"|`. - Added unit tests to validate the various ways you can write unbound data- attributes. - Updated the BasicTagHelpers codegeneration test to intermix some unbound data- attributes. #342 --- .../Parser/HtmlMarkupParser.Block.cs | 7 + .../Generator/CSharpTagHelperRenderingTest.cs | 13 +- .../TagHelpers/TagHelperBlockRewriterTest.cs | 159 ++++++++++++++++++ ...TagHelpers.CustomAttributeCodeGenerator.cs | 19 ++- .../CS/Output/BasicTagHelpers.DesignTime.cs | 5 + .../CS/Output/BasicTagHelpers.cs | 19 ++- .../CS/Source/BasicTagHelpers.cshtml | 6 +- 7 files changed, 214 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs index ef2a330946..005a2ee972 100644 --- a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs +++ b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs @@ -553,8 +553,15 @@ namespace Microsoft.AspNet.Razor.Parser } else { + // Output the attribute name, the equals and optional quote. Ex: foo=" + Output(SpanKind.Markup); + // Not a "conditional" attribute, so just read the value SkipToAndParseCode(sym => IsEndOfAttributeValue(quote, sym)); + + // Output the attribute value (will include everything in-between the attribute's quotes). + Output(SpanKind.Markup); + if (quote != HtmlSymbolType.Unknown) { Optional(quote); diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs index 2ed5f01f98..94594412dc 100644 --- a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs @@ -325,10 +325,17 @@ namespace Microsoft.AspNet.Razor.Test.Generator generatedLineIndex: 15, characterOffsetIndex: 14, contentLength: 17), - BuildLineMapping(documentAbsoluteIndex: 195, + BuildLineMapping(documentAbsoluteIndex: 202, + documentLineIndex: 5, + documentCharacterOffsetIndex: 38, + generatedAbsoluteIndex: 1300, + generatedLineIndex: 40, + generatedCharacterOffsetIndex: 6, + contentLength: 23), + BuildLineMapping(documentAbsoluteIndex: 287, documentLineIndex: 6, - generatedAbsoluteIndex: 1580, - generatedLineIndex: 44, + generatedAbsoluteIndex: 1677, + generatedLineIndex: 49, characterOffsetIndex: 40, contentLength: 4) } diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs index 71cfeaea82..7860db7af0 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs @@ -14,6 +14,165 @@ namespace Microsoft.AspNet.Razor.TagHelpers { public class TagHelperBlockRewriterTest : TagHelperRewritingTestBase { + public static TheoryData DataDashAttributeData_Document + { + get + { + var factory = CreateDefaultSpanFactory(); + var dateTimeNowString = "@DateTime.Now"; + var dateTimeNow = new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)); + + // documentContent, expectedOutput + return new TheoryData + { + { + $"", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair( + "data-required", + new MarkupBlock(dateTimeNow)), + })) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("data-required", factory.Markup("value")), + })) + }, + { + $"", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair( + "data-required", + new MarkupBlock(factory.Markup("prefix "), dateTimeNow)), + })) + }, + { + $"", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair( + "data-required", + new MarkupBlock(dateTimeNow, factory.Markup(" suffix"))), + })) + }, + { + $"", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair( + "data-required", + new MarkupBlock( + factory.Markup("prefix "), + dateTimeNow, + factory.Markup(" suffix"))), + })) + }, + { + $"", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair("pre-attribute", value: null), + new KeyValuePair( + "data-required", + new MarkupBlock( + factory.Markup("prefix "), + dateTimeNow, + factory.Markup(" suffix"))), + new KeyValuePair("post-attribute", value: null), + })) + }, + { + $"", + new MarkupBlock( + new MarkupTagHelperBlock( + "input", + selfClosing: true, + attributes: new List>() + { + new KeyValuePair( + "data-required", + new MarkupBlock( + dateTimeNow, + factory.Markup(" middle "), + dateTimeNow)), + })) + }, + }; + } + } + + public static TheoryData DataDashAttributeData_CSharpBlock + { + get + { + var factory = CreateDefaultSpanFactory(); + var documentData = DataDashAttributeData_Document; + Func, MarkupBlock> buildStatementBlock = (insideBuilder) => + { + return new MarkupBlock( + factory.EmptyHtml(), + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + insideBuilder(), + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()); + }; + + foreach (var data in documentData) + { + data[0] = $"@{{{data[0]}}}"; + data[1] = buildStatementBlock(() => data[1] as MarkupBlock); + } + + return documentData; + } + } + + [Theory] + [MemberData(nameof(DataDashAttributeData_Document))] + [MemberData(nameof(DataDashAttributeData_CSharpBlock))] + public void Rewrite_GeneratesExpectedOutputForUnboundDataDashAttributes( + string documentContent, + MarkupBlock expectedOutput) + { + // Act & Assert + RunParseTreeRewriterTest(documentContent, expectedOutput, Enumerable.Empty(), "input"); + } + public static TheoryData MinimizedAttributeData_Document { get diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.CustomAttributeCodeGenerator.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.CustomAttributeCodeGenerator.cs index 5d8925a86e..f5a204d50b 100644 --- a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.CustomAttributeCodeGenerator.cs +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.CustomAttributeCodeGenerator.cs @@ -1,4 +1,4 @@ -#pragma checksum "BasicTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "90382b3c748e0f948dfb6b452b775ba3024b2fe6" +#pragma checksum "BasicTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "d83a512ddca8f28897c27630e252991c84555533" namespace TestOutput { using Microsoft.AspNet.Razor.Runtime.TagHelpers; @@ -25,8 +25,8 @@ namespace TestOutput public override async Task ExecuteAsync() { __tagHelperRunner = __tagHelperRunner ?? new TagHelperRunner(); - Instrumentation.BeginContext(33, 49, true); - WriteLiteral("\r\n
\r\n "); + Instrumentation.BeginContext(33, 71, true); + WriteLiteral("\r\n
\r\n "); Instrumentation.EndContext(); __tagHelperExecutionContext = __tagHelperScopeManager.Begin("p", false, "test", async() => { WriteLiteral("\r\n "); @@ -49,6 +49,16 @@ namespace TestOutput __InputTagHelper2 = CreateTagHelper(); __tagHelperExecutionContext.Add(__InputTagHelper2); __InputTagHelper2.Type = __InputTagHelper.Type; + StartTagHelperWritingScope(); + WriteLiteral("2000 + "); +#line 6 "BasicTagHelpers.cshtml" +Write(ViewBag.DefaultInterval); + +#line default +#line hidden + WriteLiteral(" + 1"); + __tagHelperStringValueBuffer = EndTagHelperWritingScope(); + __tagHelperExecutionContext.AddHtmlAttribute("data-interval", Html.Raw(__tagHelperStringValueBuffer.ToString())); __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); await WriteTagHelperAsync(__tagHelperExecutionContext); __tagHelperExecutionContext = __tagHelperScopeManager.End(); @@ -78,10 +88,11 @@ namespace TestOutput __PTagHelper = CreateTagHelper(); __tagHelperExecutionContext.Add(__PTagHelper); __tagHelperExecutionContext.AddHtmlAttribute("class", Html.Raw("Hello World")); + __tagHelperExecutionContext.AddHtmlAttribute("data-delay", Html.Raw("1000")); __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); await WriteTagHelperAsync(__tagHelperExecutionContext); __tagHelperExecutionContext = __tagHelperScopeManager.End(); - Instrumentation.BeginContext(212, 8, true); + Instrumentation.BeginContext(304, 8, true); WriteLiteral("\r\n
"); Instrumentation.EndContext(); } diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.DesignTime.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.DesignTime.cs index 57b3193d28..299eab8d4a 100644 --- a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.DesignTime.cs +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.DesignTime.cs @@ -37,6 +37,11 @@ namespace TestOutput __InputTagHelper.Type = "text"; __InputTagHelper2 = CreateTagHelper(); __InputTagHelper2.Type = __InputTagHelper.Type; +#line 6 "BasicTagHelpers.cshtml" +__o = ViewBag.DefaultInterval; + +#line default +#line hidden __InputTagHelper = CreateTagHelper(); __InputTagHelper.Type = "checkbox"; __InputTagHelper2 = CreateTagHelper(); diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.cs index b1b35018f7..ad20384e5c 100644 --- a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.cs +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.cs @@ -1,4 +1,4 @@ -#pragma checksum "BasicTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "90382b3c748e0f948dfb6b452b775ba3024b2fe6" +#pragma checksum "BasicTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "d83a512ddca8f28897c27630e252991c84555533" namespace TestOutput { using Microsoft.AspNet.Razor.Runtime.TagHelpers; @@ -26,8 +26,8 @@ namespace TestOutput public override async Task ExecuteAsync() { __tagHelperRunner = __tagHelperRunner ?? new TagHelperRunner(); - Instrumentation.BeginContext(33, 49, true); - WriteLiteral("\r\n
\r\n "); + Instrumentation.BeginContext(33, 71, true); + WriteLiteral("\r\n
\r\n "); Instrumentation.EndContext(); __tagHelperExecutionContext = __tagHelperScopeManager.Begin("p", false, "test", async() => { WriteLiteral("\r\n "); @@ -50,6 +50,16 @@ namespace TestOutput __InputTagHelper2 = CreateTagHelper(); __tagHelperExecutionContext.Add(__InputTagHelper2); __InputTagHelper2.Type = __InputTagHelper.Type; + StartTagHelperWritingScope(); + WriteLiteral("2000 + "); +#line 6 "BasicTagHelpers.cshtml" +Write(ViewBag.DefaultInterval); + +#line default +#line hidden + WriteLiteral(" + 1"); + __tagHelperStringValueBuffer = EndTagHelperWritingScope(); + __tagHelperExecutionContext.AddHtmlAttribute("data-interval", Html.Raw(__tagHelperStringValueBuffer.ToString())); __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); await WriteTagHelperAsync(__tagHelperExecutionContext); __tagHelperExecutionContext = __tagHelperScopeManager.End(); @@ -79,10 +89,11 @@ namespace TestOutput __PTagHelper = CreateTagHelper(); __tagHelperExecutionContext.Add(__PTagHelper); __tagHelperExecutionContext.AddHtmlAttribute("class", Html.Raw("Hello World")); + __tagHelperExecutionContext.AddHtmlAttribute("data-delay", Html.Raw("1000")); __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); await WriteTagHelperAsync(__tagHelperExecutionContext); __tagHelperExecutionContext = __tagHelperScopeManager.End(); - Instrumentation.BeginContext(212, 8, true); + Instrumentation.BeginContext(304, 8, true); WriteLiteral("\r\n
"); Instrumentation.EndContext(); } diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/BasicTagHelpers.cshtml b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/BasicTagHelpers.cshtml index 043f080395..30e65e0aae 100644 --- a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/BasicTagHelpers.cshtml +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/BasicTagHelpers.cshtml @@ -1,9 +1,9 @@ @addTagHelper "something, nice" -
-

+

+

- +

\ No newline at end of file