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:
parent
277e1b4702
commit
fd5426943f
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue