Merge sibling nodes during markup block rewrite

This change adds the ability to merge sibling nodes when possible during
markup block rewriting. We retain that invariant that each markup block
is a valid chunk of markup containing properly nested tags.

We still haven't done any work to remove whitespace yet, so most of the
cases where this comes into play right now will merge an element with
its surrounding whitespace.
This commit is contained in:
Ryan Nowak 2018-08-10 16:11:16 -07:00 committed by Ryan Nowak
parent 277e1b4702
commit fd5426943f
16 changed files with 232 additions and 43 deletions

View File

@ -44,8 +44,59 @@ namespace Microsoft.AspNetCore.Blazor.Razor
// Forcibly remove a node to prevent infinite loops.
trees.RemoveAt(trees.Count - 1);
rewriteVisitor.Visit(reference.Node);
reference.Replace(new HtmlBlockIntermediateNode()
// We want to fold together siblings where possible. To do this, first we find
// the index of the node we're looking at now - then we need to walk backwards
// and identify a set of contiguous nodes we can merge.
var start = reference.Parent.Children.Count - 1;
for (; start >= 0; start--)
{
if (ReferenceEquals(reference.Node, reference.Parent.Children[start]))
{
break;
}
}
// This is the current node. Check if the left sibling is always a candidate
// for rewriting. Due to the order we processed the nodes, we know that the
// left sibling is next in the list to process if it's a candidate.
var end = start;
while (start - 1 >= 0)
{
var candidate = reference.Parent.Children[start - 1];
if (trees.Count == 0 || !ReferenceEquals(trees[trees.Count - 1].Node, candidate))
{
// This means the we're out of nodes, or the left sibling is not in the list.
break;
}
// This means that the left sibling is valid to merge.
start--;
// Remove this since we're combining it.
trees.RemoveAt(trees.Count - 1);
}
// As a degenerate case, don't bother rewriting an single HtmlContent node
// It doesn't add any value.
if (end - start == 0 && reference.Node is HtmlContentIntermediateNode)
{
continue;
}
// Now we know the range of nodes to rewrite (end is inclusive)
var length = end + 1 - start;
while (length > 0)
{
// Keep using start since we're removing nodes.
var node = reference.Parent.Children[start];
reference.Parent.Children.RemoveAt(start);
rewriteVisitor.Visit(node);
length--;
}
reference.Parent.Children.Insert(start, new HtmlBlockIntermediateNode()
{
Content = rewriteVisitor.Builder.ToString(),
});
@ -138,14 +189,28 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public override void VisitHtml(HtmlContentIntermediateNode node)
{
// We need to restore the state after processing this node.
// We might have found a leaf-block of HTML, but that shouldn't
// affect our parent's state.
var originalState = _foundNonHtml;
_foundNonHtml = false;
if (node.HasDiagnostics)
{
// Treat node with errors as non-HTML
_foundNonHtml = true;
}
// Visit Children
base.VisitDefault(node);
if (!_foundNonHtml)
{
Trees.Add(new IntermediateNodeReference(Parent, node));
}
_foundNonHtml = originalState |= _foundNonHtml;
}
public override void VisitToken(IntermediateToken node)

View File

@ -334,6 +334,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
case RenderTreeFrameType.Text:
return $"Text: (seq={Sequence}, len=n/a) {EscapeNewlines(TextContent)}";
case RenderTreeFrameType.Markup:
return $"Markup: (seq={Sequence}, len=n/a) {EscapeNewlines(TextContent)}";
case RenderTreeFrameType.ElementReferenceCapture:
return $"ElementReferenceCapture: (seq={Sequence}, len=n/a) {ElementReferenceCaptureAction}";
}

View File

@ -488,5 +488,71 @@ namespace Test
frames,
frame => AssertFrame.Text(frame, "<span>Hi</span>"));
}
// Integration test for HTML block rewriting
[Fact]
public void Render_HtmlBlock_Integration()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
RenderFragment ChildContent { get; set; }
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<html>
<head><meta><meta></head>
<body>
<MyComponent>
<div><span></span><span></span></div>
<div>@(""hi"")</div>
<div><span></span><span></span></div>
<div></div>
<div>@(""hi"")</div>
<div></div>
</MyComponent>
</body>
</html>");
// Act
var frames = GetRenderTree(component);
// Assert: component frames are correct
Assert.Collection(
frames,
frame => AssertFrame.Element(frame, "html", 9, 0),
frame => AssertFrame.Whitespace(frame, 1),
frame => AssertFrame.Markup(frame, "<head><meta><meta></head>\n ", 2),
frame => AssertFrame.Element(frame, "body", 5, 3),
frame => AssertFrame.Whitespace(frame, 4),
frame => AssertFrame.Component(frame, "Test.MyComponent", 2, 5),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 6),
frame => AssertFrame.Whitespace(frame, 16),
frame => AssertFrame.Whitespace(frame, 17));
// Assert: Captured ChildContent frames are correct
var childFrames = GetFrames((RenderFragment)frames[6].AttributeValue);
Assert.Collection(
childFrames,
frame => AssertFrame.Whitespace(frame, 7),
frame => AssertFrame.Markup(frame, "<div><span></span><span></span></div>\n ", 8),
frame => AssertFrame.Element(frame, "div", 2, 9),
frame => AssertFrame.Text(frame, "hi", 10),
frame => AssertFrame.Whitespace(frame, 11),
frame => AssertFrame.Markup(frame, "<div><span></span><span></span></div>\n <div></div>\n ", 12),
frame => AssertFrame.Element(frame, "div", 2, 13),
frame => AssertFrame.Text(frame, "hi", 14),
frame => AssertFrame.Markup(frame, "\n <div></div>\n ", 15));
}
}
}

View File

@ -175,9 +175,9 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
var frames = GetRenderTree(component);
// Assert
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "Start", 0),
frame => AssertFrame.Text(frame, "End", 1));
Assert.Collection(
frames,
frame => AssertFrame.Markup(frame, "StartEnd", 0));
}
[Fact]

View File

@ -19,11 +19,10 @@ namespace Test
builder.AddAttribute(1, "SomeProp", "val");
builder.AddAttribute(2, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
builder2.AddContent(3, "\n Some ");
builder2.AddMarkupContent(4, "<el>further</el>");
builder2.AddContent(5, " content\n");
builder2.AddMarkupContent(4, "<el>further</el> content\n");
}
));
builder.AddComponentReferenceCapture(6, (__value) => {
builder.AddComponentReferenceCapture(5, (__value) => {
#line 2 "x:\dir\subdir\Test\TestComponent.cshtml"
myInstance = (Test.MyComponent)__value;

View File

@ -13,9 +13,7 @@ Document -
ComponentExtensionNode - (31:1,0 [96] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - Test.MyComponent
HtmlContent - (76:1,45 [11] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (76:1,45 [11] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n Some
HtmlBlock - - <el>further</el>
HtmlContent - (103:2,25 [10] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (103:2,25 [10] x:\dir\subdir\Test\TestComponent.cshtml) - Html - content\n
HtmlBlock - - <el>further</el> content\n
RefExtensionNode - (49:1,18 [10] x:\dir\subdir\Test\TestComponent.cshtml) - myInstance - Test.MyComponent
ComponentAttributeExtensionNode - - SomeProp -
HtmlContent - (71:1,40 [3] x:\dir\subdir\Test\TestComponent.cshtml)

View File

@ -1,13 +1,13 @@
Source Location: (49:1,18 [10] x:\dir\subdir\Test\TestComponent.cshtml)
|myInstance|
Generated Location: (1176:28,18 [10] )
Generated Location: (1131:27,18 [10] )
|myInstance|
Source Location: (143:5,12 [44] x:\dir\subdir\Test\TestComponent.cshtml)
|
private Test.MyComponent myInstance;
|
Generated Location: (1430:38,12 [44] )
Generated Location: (1385:37,12 [44] )
|
private Test.MyComponent myInstance;
|

View File

@ -16,10 +16,9 @@ namespace Test
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.AddMarkupContent(0, "<h1>Hello, world!</h1>");
builder.AddContent(1, "\n\nWelcome to your new app.\n\n");
builder.OpenComponent<Test.SurveyPrompt>(2);
builder.AddAttribute(3, "Title", "");
builder.AddMarkupContent(0, "<h1>Hello, world!</h1>\n\nWelcome to your new app.\n\n");
builder.OpenComponent<Test.SurveyPrompt>(1);
builder.AddAttribute(2, "Title", "");
builder.CloseComponent();
}
#pragma warning restore 1998

View File

@ -11,9 +11,7 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlBlock - - <h1>Hello, world!</h1>
HtmlContent - (66:3,22 [32] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (66:3,22 [32] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\nWelcome to your new app.\n\n
HtmlBlock - - <h1>Hello, world!</h1>\n\nWelcome to your new app.\n\n
ComponentExtensionNode - (98:7,0 [23] x:\dir\subdir\Test\TestComponent.cshtml) - SurveyPrompt - Test.SurveyPrompt
ComponentAttributeExtensionNode - (119:7,21 [0] x:\dir\subdir\Test\TestComponent.cshtml) - Title - Title
HtmlContent - (119:7,21 [0] x:\dir\subdir\Test\TestComponent.cshtml)

View File

@ -16,10 +16,9 @@ namespace Test
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.AddMarkupContent(0, "<h1>Hello, world!</h1>");
builder.AddContent(1, "\n\nWelcome to your new app.\n\n");
builder.OpenComponent<Test.SurveyPrompt>(2);
builder.AddAttribute(3, "Title", "<div>Test!</div>");
builder.AddMarkupContent(0, "<h1>Hello, world!</h1>\n\nWelcome to your new app.\n\n");
builder.OpenComponent<Test.SurveyPrompt>(1);
builder.AddAttribute(2, "Title", "<div>Test!</div>");
builder.CloseComponent();
}
#pragma warning restore 1998

View File

@ -11,9 +11,7 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlBlock - - <h1>Hello, world!</h1>
HtmlContent - (66:3,22 [32] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (66:3,22 [32] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\nWelcome to your new app.\n\n
HtmlBlock - - <h1>Hello, world!</h1>\n\nWelcome to your new app.\n\n
ComponentExtensionNode - (98:7,0 [41] x:\dir\subdir\Test\TestComponent.cshtml) - SurveyPrompt - Test.SurveyPrompt
ComponentAttributeExtensionNode - (119:7,21 [16] x:\dir\subdir\Test\TestComponent.cshtml) - Title - Title
HtmlContent - (119:7,21 [16] x:\dir\subdir\Test\TestComponent.cshtml)

View File

@ -15,9 +15,8 @@ namespace Test
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.AddMarkupContent(0, "<h1>Hello</h1>");
builder.AddContent(1, "\n\n");
builder.AddContent(2, "My value");
builder.AddMarkupContent(0, "<h1>Hello</h1>\n\n");
builder.AddContent(1, "My value");
}
#pragma warning restore 1998
}

View File

@ -10,8 +10,6 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlBlock - - <h1>Hello</h1>
HtmlContent - (14:0,14 [4] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (14:0,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n
HtmlBlock - - <h1>Hello</h1>\n\n
CSharpExpression - (20:2,2 [10] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (20:2,2 [10] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - "My value"

View File

@ -15,9 +15,8 @@ namespace Test
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.AddMarkupContent(0, "<h1>Hello</h1>");
builder.AddContent(1, "\n\n");
builder.OpenComponent<Test.SomeOtherComponent>(2);
builder.AddMarkupContent(0, "<h1>Hello</h1>\n\n");
builder.OpenComponent<Test.SomeOtherComponent>(1);
builder.CloseComponent();
}
#pragma warning restore 1998

View File

@ -10,7 +10,5 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlBlock - - <h1>Hello</h1>
HtmlContent - (45:1,14 [4] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (45:1,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n
HtmlBlock - - <h1>Hello</h1>\n\n
ComponentExtensionNode - (49:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - SomeOtherComponent - Test.SomeOtherComponent

View File

@ -64,6 +64,76 @@ namespace Microsoft.AspNetCore.Blazor.Razor
Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
}
[Fact]
public void Execute_RewritesHtml_WithComment()
{
// Arrange
var document = CreateDocument(@"Start<!-- -->End");
var expected = NormalizeContent(@"StartEnd");
var documentNode = Lower(document);
// Act
Pass.Execute(document, documentNode);
// Assert
var block = documentNode.FindDescendantNodes<HtmlBlockIntermediateNode>().Single();
Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
}
[Fact]
public void Execute_RewritesHtml_MergesSiblings()
{
// Arrange
var document = CreateDocument(@"
<html>
@(""Hi"")<div></div>
<div></div>
<div>@(""Hi"")</div>
</html>");
var expected = NormalizeContent(@"
<div></div>
<div></div>
");
var documentNode = Lower(document);
// Act
Pass.Execute(document, documentNode);
// Assert
var block = documentNode.FindDescendantNodes<HtmlBlockIntermediateNode>().Single();
Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
}
[Fact]
public void Execute_RewritesHtml_MergesSiblings_LeftEdge()
{
// Arrange
var document = CreateDocument(@"
<html><div></div>
<div></div>
<div>@(""Hi"")</div>
</html>");
var expected = NormalizeContent(@"
<div></div>
<div></div>
");
var documentNode = Lower(document);
// Act
Pass.Execute(document, documentNode);
// Assert
var block = documentNode.FindDescendantNodes<HtmlBlockIntermediateNode>().Single();
Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
}
[Fact]
public void Execute_RewritesHtml_CSharpInAttributes()
{
@ -75,7 +145,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
</head>
</html>");
var expected = NormalizeContent(@"<div>foo</div>");
var expected = NormalizeContent("<div>foo</div>\n ");
var documentNode = Lower(document);
@ -100,7 +170,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
</head>
</html>");
var expected = NormalizeContent(@"<div>rewriteme</div>");
var expected = NormalizeContent("<div>rewriteme</div>\n ");
var documentNode = Lower(document);
@ -248,7 +318,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
</span>
</html>");
var expected = NormalizeContent(@"<div>rewriteme</div>");
var expected = NormalizeContent("<div>rewriteme</div>\n ");
var documentNode = Lower(document);