In RazorCompiler, support attributes with C# expression values

This commit is contained in:
Steve Sanderson 2018-01-16 13:15:13 +00:00
parent 5aa6ccaff7
commit b690aeee28
2 changed files with 95 additions and 18 deletions

View File

@ -28,6 +28,10 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
new[] { "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" }, new[] { "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" },
StringComparer.OrdinalIgnoreCase); StringComparer.OrdinalIgnoreCase);
private string _unconsumedHtml;
private string _currentAttributeName;
private IDictionary<string, object> _currentElementAttributes = new Dictionary<string, object>();
public override void BeginWriterScope(CodeRenderingContext context, string writer) public override void BeginWriterScope(CodeRenderingContext context, string writer)
{ {
throw new System.NotImplementedException(nameof(BeginWriterScope)); throw new System.NotImplementedException(nameof(BeginWriterScope));
@ -101,12 +105,19 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node) public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node)
{ {
throw new System.NotImplementedException(nameof(WriteCSharpExpressionAttributeValue)); if (string.IsNullOrEmpty(_currentAttributeName))
{
throw new InvalidOperationException($"Invoked {nameof(WriteCSharpCodeAttributeValue)} while {nameof(_currentAttributeName)} was null or empty.");
}
_currentElementAttributes[_currentAttributeName] = node.Children.Single();
} }
public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node) public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node)
{ {
throw new System.NotImplementedException(nameof(WriteHtmlAttribute)); _currentAttributeName = node.AttributeName;
context.RenderChildren(node);
_currentAttributeName = null;
} }
public override void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node) public override void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node)
@ -117,9 +128,16 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node) public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node)
{ {
var originalHtmlContent = GetContent(node); var originalHtmlContent = GetContent(node);
if (_unconsumedHtml != null)
{
originalHtmlContent = _unconsumedHtml + originalHtmlContent;
_unconsumedHtml = null;
}
var tokenizer = new HtmlTokenizer( var tokenizer = new HtmlTokenizer(
new TextSource(originalHtmlContent), new TextSource(originalHtmlContent),
HtmlEntityService.Resolver); HtmlEntityService.Resolver);
var codeWriter = context.CodeWriter;
HtmlToken nextToken; HtmlToken nextToken;
while ((nextToken = tokenizer.Get()).Type != HtmlTokenType.EndOfFile) while ((nextToken = tokenizer.Get()).Type != HtmlTokenType.EndOfFile)
@ -129,7 +147,7 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
case HtmlTokenType.Character: case HtmlTokenType.Character:
{ {
// Text node // Text node
context.CodeWriter codeWriter
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddText)}") .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddText)}")
.WriteStringLiteral(nextToken.Data) .WriteStringLiteral(nextToken.Data)
.WriteEndMethodInvocation(); .WriteEndMethodInvocation();
@ -142,7 +160,7 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
var nextTag = nextToken.AsTag(); var nextTag = nextToken.AsTag();
if (nextToken.Type == HtmlTokenType.StartTag) if (nextToken.Type == HtmlTokenType.StartTag)
{ {
context.CodeWriter codeWriter
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenElement)}") .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenElement)}")
.WriteStringLiteral(nextTag.Data) .WriteStringLiteral(nextTag.Data)
.WriteEndMethodInvocation(); .WriteEndMethodInvocation();
@ -150,19 +168,23 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
foreach (var attribute in nextTag.Attributes) foreach (var attribute in nextTag.Attributes)
{ {
context.CodeWriter WriteAttribute(codeWriter, attribute.Key, attribute.Value);
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}") }
.WriteStringLiteral(attribute.Key)
.WriteParameterSeparator() if (_currentElementAttributes.Count > 0)
.WriteStringLiteral(attribute.Value) {
.WriteEndMethodInvocation(); foreach (var pair in _currentElementAttributes)
{
WriteAttribute(codeWriter, pair.Key, pair.Value);
}
_currentElementAttributes.Clear();
} }
if (nextToken.Type == HtmlTokenType.EndTag if (nextToken.Type == HtmlTokenType.EndTag
|| nextTag.IsSelfClosing || nextTag.IsSelfClosing
|| htmlVoidElementsLookup.Contains(nextTag.Data)) || htmlVoidElementsLookup.Contains(nextTag.Data))
{ {
context.CodeWriter codeWriter
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.CloseElement)}") .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.CloseElement)}")
.WriteEndMethodInvocation(); .WriteEndMethodInvocation();
} }
@ -174,7 +196,48 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
} }
} }
// If we got an EOF in the middle of an HTML element, it's probably because we're
// about to receive some attribute name/value pairs. Store the unused HTML content
// so we can prepend it to the part that comes after the attributes to make
// complete valid markup.
if (originalHtmlContent.Length > nextToken.Position.Position)
{
_unconsumedHtml = originalHtmlContent.Substring(nextToken.Position.Position - 1);
}
}
private static void WriteAttribute(CodeWriter codeWriter, string key, object value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
codeWriter
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
.WriteStringLiteral(key)
.WriteParameterSeparator();
switch (value)
{
case IntermediateToken intermediateToken:
{
if (!intermediateToken.IsCSharp)
{
throw new ArgumentException($"Not yet supported: IntermediateToken where IsCSharp==false");
}
codeWriter.Write(intermediateToken.Content);
break;
}
case string valueString:
codeWriter.WriteStringLiteral(valueString);
break;
default:
throw new ArgumentException($"Unsupported attribute value type: {value.GetType().FullName}");
}
codeWriter.WriteEndMethodInvocation();
} }
public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node) public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)

View File

@ -86,7 +86,7 @@ namespace Microsoft.Blazor.Build.Test
} }
[Fact] [Fact]
public void CanRenderPlainText() public void SupportsPlainText()
{ {
// Arrange // Arrange
var treeBuilder = new RenderTreeBuilder(new TestRenderer()); var treeBuilder = new RenderTreeBuilder(new TestRenderer());
@ -101,7 +101,7 @@ namespace Microsoft.Blazor.Build.Test
} }
[Fact] [Fact]
public void CanUseCSharpFunctionsBlock() public void SupportsCSharpFunctionsBlock()
{ {
// Arrange/Act // Arrange/Act
var component = CompileToComponent(@" var component = CompileToComponent(@"
@ -122,7 +122,7 @@ namespace Microsoft.Blazor.Build.Test
} }
[Fact] [Fact]
public void CanRenderElements() public void SupportsElements()
{ {
// Arrange/Act // Arrange/Act
var component = CompileToComponent("<myelem>Hello</myelem>"); var component = CompileToComponent("<myelem>Hello</myelem>");
@ -134,7 +134,7 @@ namespace Microsoft.Blazor.Build.Test
} }
[Fact] [Fact]
public void CanRenderSelfClosingElements() public void SupportsSelfClosingElements()
{ {
// Arrange/Act // Arrange/Act
var component = CompileToComponent("Some text so elem isn't at position 0 <myelem />"); var component = CompileToComponent("Some text so elem isn't at position 0 <myelem />");
@ -146,7 +146,7 @@ namespace Microsoft.Blazor.Build.Test
} }
[Fact] [Fact]
public void CanRenderVoidHtmlElements() public void SupportsVoidHtmlElements()
{ {
// Arrange/Act // Arrange/Act
var component = CompileToComponent("Some text so elem isn't at position 0 <img>"); var component = CompileToComponent("Some text so elem isn't at position 0 <img>");
@ -158,7 +158,7 @@ namespace Microsoft.Blazor.Build.Test
} }
[Fact] [Fact]
public void CanRenderAttributes() public void SupportsAttributesWithLiteralValues()
{ {
// Arrange/Act // Arrange/Act
var component = CompileToComponent("<elem attrib-one=\"Value 1\" a2='v2' />"); var component = CompileToComponent("<elem attrib-one=\"Value 1\" a2='v2' />");
@ -170,6 +170,20 @@ namespace Microsoft.Blazor.Build.Test
node => AssertNode.Attribute(node, "a2", "v2")); node => AssertNode.Attribute(node, "a2", "v2"));
} }
[Fact]
public void SupportsAttributesWithStringExpressionValues()
{
// Arrange/Act
var component = CompileToComponent(
"@{ var myValue = \"My string\"; }"
+ "<elem attr=@myValue />");
// Assert
Assert.Collection(GetRenderTree(component).Where(NotWhitespace),
node => AssertNode.Element(node, "elem", 1),
node => AssertNode.Attribute(node, "attr", "My string"));
}
private static bool NotWhitespace(RenderTreeNode node) private static bool NotWhitespace(RenderTreeNode node)
=> node.NodeType != RenderTreeNodeType.Text => node.NodeType != RenderTreeNodeType.Text
|| !string.IsNullOrWhiteSpace(node.TextContent); || !string.IsNullOrWhiteSpace(node.TextContent);