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
This commit is contained in:
N. Taylor Mullen 2015-05-17 17:07:51 -07:00
parent 407a2ceae6
commit b25bf01158
7 changed files with 214 additions and 14 deletions

View File

@ -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);

View File

@ -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)
}

View File

@ -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<string, MarkupBlock>
{
{
$"<input data-required='{dateTimeNowString}' />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>()
{
new KeyValuePair<string, SyntaxTreeNode>(
"data-required",
new MarkupBlock(dateTimeNow)),
}))
},
{
"<input data-required='value' />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>()
{
new KeyValuePair<string, SyntaxTreeNode>("data-required", factory.Markup("value")),
}))
},
{
$"<input data-required='prefix {dateTimeNowString}' />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>()
{
new KeyValuePair<string, SyntaxTreeNode>(
"data-required",
new MarkupBlock(factory.Markup("prefix "), dateTimeNow)),
}))
},
{
$"<input data-required='{dateTimeNowString} suffix' />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>()
{
new KeyValuePair<string, SyntaxTreeNode>(
"data-required",
new MarkupBlock(dateTimeNow, factory.Markup(" suffix"))),
}))
},
{
$"<input data-required='prefix {dateTimeNowString} suffix' />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>()
{
new KeyValuePair<string, SyntaxTreeNode>(
"data-required",
new MarkupBlock(
factory.Markup("prefix "),
dateTimeNow,
factory.Markup(" suffix"))),
}))
},
{
$"<input pre-attribute data-required='prefix {dateTimeNowString} suffix' post-attribute />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>()
{
new KeyValuePair<string, SyntaxTreeNode>("pre-attribute", value: null),
new KeyValuePair<string, SyntaxTreeNode>(
"data-required",
new MarkupBlock(
factory.Markup("prefix "),
dateTimeNow,
factory.Markup(" suffix"))),
new KeyValuePair<string, SyntaxTreeNode>("post-attribute", value: null),
}))
},
{
$"<input data-required='{dateTimeNowString} middle {dateTimeNowString}' />",
new MarkupBlock(
new MarkupTagHelperBlock(
"input",
selfClosing: true,
attributes: new List<KeyValuePair<string, SyntaxTreeNode>>()
{
new KeyValuePair<string, SyntaxTreeNode>(
"data-required",
new MarkupBlock(
dateTimeNow,
factory.Markup(" middle "),
dateTimeNow)),
}))
},
};
}
}
public static TheoryData DataDashAttributeData_CSharpBlock
{
get
{
var factory = CreateDefaultSpanFactory();
var documentData = DataDashAttributeData_Document;
Func<Func<MarkupBlock>, 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<RazorError>(), "input");
}
public static TheoryData MinimizedAttributeData_Document
{
get

View File

@ -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<div class=\"randomNonTagHelperAttribute\">\r\n ");
Instrumentation.BeginContext(33, 71, true);
WriteLiteral("\r\n<div data-animation=\"fade\" class=\"randomNonTagHelperAttribute\">\r\n ");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("p", false, "test", async() => {
WriteLiteral("\r\n ");
@ -49,6 +49,16 @@ namespace TestOutput
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__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<PTagHelper>();
__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</div>");
Instrumentation.EndContext();
}

View File

@ -37,6 +37,11 @@ namespace TestOutput
__InputTagHelper.Type = "text";
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__InputTagHelper2.Type = __InputTagHelper.Type;
#line 6 "BasicTagHelpers.cshtml"
__o = ViewBag.DefaultInterval;
#line default
#line hidden
__InputTagHelper = CreateTagHelper<InputTagHelper>();
__InputTagHelper.Type = "checkbox";
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();

View File

@ -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<div class=\"randomNonTagHelperAttribute\">\r\n ");
Instrumentation.BeginContext(33, 71, true);
WriteLiteral("\r\n<div data-animation=\"fade\" class=\"randomNonTagHelperAttribute\">\r\n ");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("p", false, "test", async() => {
WriteLiteral("\r\n ");
@ -50,6 +50,16 @@ namespace TestOutput
__InputTagHelper2 = CreateTagHelper<InputTagHelper2>();
__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<PTagHelper>();
__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</div>");
Instrumentation.EndContext();
}

View File

@ -1,9 +1,9 @@
@addTagHelper "something, nice"
<div class="randomNonTagHelperAttribute">
<p class="Hello World">
<div data-animation="fade" class="randomNonTagHelperAttribute">
<p class="Hello World" data-delay="1000">
<p></p>
<input type="text" />
<input data-interval="2000 + @ViewBag.DefaultInterval + 1" type="text" />
<input type="checkbox" checked="true"/>
</p>
</div>