diff --git a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs index 4b1c904906..88e8393930 100644 --- a/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs +++ b/src/Microsoft.Blazor.Build/Core/RazorCompilation/Engine/BlazorIntermediateNodeWriter.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Razor.Language.CodeGeneration; using Microsoft.AspNetCore.Razor.Language.Intermediate; using Microsoft.Blazor.RenderTree; using System; +using System.Collections.Generic; using System.Linq; using System.Text; @@ -20,6 +21,13 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine { private const string builderVarName = "builder"; + // Per the HTML spec, the following elements are inherently self-closing + // For example, is the same as (and therefore it cannot contain descendants) + private static HashSet htmlVoidElementsLookup + = new HashSet( + new[] { "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" }, + StringComparer.OrdinalIgnoreCase); + public override void BeginWriterScope(CodeRenderingContext context, string writer) { throw new System.NotImplementedException(nameof(BeginWriterScope)); @@ -119,26 +127,37 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine switch (nextToken.Type) { case HtmlTokenType.Character: - // Text node - context.CodeWriter - .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddText)}") - .WriteStringLiteral(nextToken.Data) - .WriteEndMethodInvocation(); - break; + { + // Text node + context.CodeWriter + .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddText)}") + .WriteStringLiteral(nextToken.Data) + .WriteEndMethodInvocation(); + break; + } case HtmlTokenType.StartTag: - var nextTag = nextToken.AsTag(); - context.CodeWriter - .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenElement)}") - .WriteStringLiteral(nextTag.Data) - .WriteEndMethodInvocation(); - break; - case HtmlTokenType.EndTag: - context.CodeWriter - .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.CloseElement)}") - .WriteEndMethodInvocation(); - break; + { + var nextTag = nextToken.AsTag(); + if (nextToken.Type == HtmlTokenType.StartTag) + { + context.CodeWriter + .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.OpenElement)}") + .WriteStringLiteral(nextTag.Data) + .WriteEndMethodInvocation(); + } + + if (nextToken.Type == HtmlTokenType.EndTag + || nextTag.IsSelfClosing + || htmlVoidElementsLookup.Contains(nextTag.Data)) + { + context.CodeWriter + .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.CloseElement)}") + .WriteEndMethodInvocation(); + } + break; + } default: throw new InvalidCastException($"Unsupported token type: {nextToken.Type.ToString()}"); diff --git a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs index 8bfba05999..ac7f730c4e 100644 --- a/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.Blazor.Build.Test/RazorCompilerTest.cs @@ -128,12 +128,35 @@ namespace Microsoft.Blazor.Build.Test var component = CompileToComponent("Hello"); // Assert - var nodes = GetRenderTree(component).Where(NotWhitespace); - Assert.Collection(nodes, + Assert.Collection(GetRenderTree(component), node => AssertNode.Element(node, "myelem", 1), node => AssertNode.Text(node, "Hello")); } + [Fact] + public void CanRenderSelfClosingElements() + { + // Arrange/Act + var component = CompileToComponent("Some text so elem isn't at position 0 "); + + // Assert + Assert.Collection(GetRenderTree(component), + node => AssertNode.Text(node, "Some text so elem isn't at position 0 "), + node => AssertNode.Element(node, "myelem", 1)); + } + + [Fact] + public void CanRenderVoidHtmlElements() + { + // Arrange/Act + var component = CompileToComponent("Some text so elem isn't at position 0 "); + + // Assert + Assert.Collection(GetRenderTree(component), + node => AssertNode.Text(node, "Some text so elem isn't at position 0 "), + node => AssertNode.Element(node, "img", 1)); + } + private static bool NotWhitespace(RenderTreeNode node) => node.NodeType != RenderTreeNodeType.Text || !string.IsNullOrWhiteSpace(node.TextContent);