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:
parent
407a2ceae6
commit
b25bf01158
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue