+ //
+ // We would store both the div and a tag in a list, but make sure to visit
+ // the div first. Then when we process the div (recursively), we would remove
+ // the a from the list.
+ private class FindHtmlTreeVisitor :
+ IntermediateNodeWalker,
+ IExtensionIntermediateNodeVisitor
+ {
+ private bool _foundNonHtml;
+
+ public List Trees { get; } = new List();
+
+ public override void VisitDefault(IntermediateNode node)
+ {
+ // If we get here, we found a non-HTML node. Keep traversing.
+ _foundNonHtml = true;
+ base.VisitDefault(node);
+ }
+
+ public void VisitExtension(HtmlElementIntermediateNode 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 - don't let the parent rewrite this either.
+ _foundNonHtml = true;
+ }
+
+ if (string.Equals("script", node.TagName, StringComparison.OrdinalIgnoreCase))
+ {
+ // Treat script tags as non-HTML - we trigger errors for script tags
+ // later.
+ _foundNonHtml = true;
+ }
+
+ base.VisitDefault(node);
+
+ if (!_foundNonHtml)
+ {
+ Trees.Add(new IntermediateNodeReference(Parent, node));
+ }
+
+ _foundNonHtml = originalState |= _foundNonHtml;
+ }
+
+ public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node)
+ {
+ if (node.HasDiagnostics)
+ {
+ // Treat node with errors as non-HTML
+ _foundNonHtml = true;
+ }
+
+ // Visit Children
+ base.VisitDefault(node);
+ }
+
+ public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node)
+ {
+ if (node.HasDiagnostics)
+ {
+ // Treat node with errors as non-HTML
+ _foundNonHtml = true;
+ }
+
+ // Visit Children
+ base.VisitDefault(node);
+ }
+
+ public override void VisitHtml(HtmlContentIntermediateNode node)
+ {
+ if (node.HasDiagnostics)
+ {
+ // Treat node with errors as non-HTML
+ _foundNonHtml = true;
+ }
+
+ // Visit Children
+ base.VisitDefault(node);
+ }
+
+ public override void VisitToken(IntermediateToken node)
+ {
+ if (node.HasDiagnostics)
+ {
+ // Treat node with errors as non-HTML
+ _foundNonHtml = true;
+ }
+
+ if (node.IsCSharp)
+ {
+ _foundNonHtml = true;
+ }
+ }
+ }
+
+ private class RewriteVisitor :
+ IntermediateNodeWalker,
+ IExtensionIntermediateNodeVisitor
+ {
+ private readonly List _trees;
+
+ public RewriteVisitor(List trees)
+ {
+ _trees = trees;
+ }
+
+ public StringBuilder Builder { get; } = new StringBuilder();
+
+ public void VisitExtension(HtmlElementIntermediateNode node)
+ {
+ for (var i = 0; i < _trees.Count; i++)
+ {
+ // Remove this node if it's in the list. This ensures that we don't
+ // do redundant operations.
+ if (ReferenceEquals(_trees[i].Node, node))
+ {
+ _trees.RemoveAt(i);
+ break;
+ }
+ }
+
+ var isVoid = ComponentDocumentRewritePass.VoidElements.Contains(node.TagName);
+ var hasBodyContent = node.Body.Any();
+
+ Builder.Append("<");
+ Builder.Append(node.TagName);
+
+ foreach (var attribute in node.Attributes)
+ {
+ Visit(attribute);
+ }
+
+ // If for some reason a void element contains body, then treat it as a
+ // start/end tag. Treat non-void elements without body content as self-closing.
+ if (!hasBodyContent && isVoid)
+ {
+ // void
+ Builder.Append(">");
+ return;
+ }
+ else if (!hasBodyContent)
+ {
+ // self-closing
+ Builder.Append("/>");
+ return;
+ }
+
+ // start/end tag with body.
+ Builder.Append(">");
+
+ foreach (var item in node.Body)
+ {
+ Visit(item);
+ }
+
+ Builder.Append("");
+ Builder.Append(node.TagName);
+ Builder.Append(">");
+ }
+
+ public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node)
+ {
+ Builder.Append(" ");
+ Builder.Append(node.AttributeName);
+
+ if (node.Children.Count == 0)
+ {
+ // Minimized attribute
+ return;
+ }
+
+ Builder.Append("=\"");
+
+ // Visit Children
+ base.VisitDefault(node);
+
+ Builder.Append("\"");
+ }
+
+ public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node)
+ {
+ // Visit Children
+ base.VisitDefault(node);
+ }
+
+ public override void VisitHtml(HtmlContentIntermediateNode node)
+ {
+ // Visit Children
+ base.VisitDefault(node);
+ }
+
+ public override void VisitToken(IntermediateToken node)
+ {
+ Builder.Append(node.Content);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RenderBatchWriter.cs b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RenderBatchWriter.cs
index 6538e81d26..9e5d36e178 100644
--- a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RenderBatchWriter.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RenderBatchWriter.cs
@@ -183,6 +183,10 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits
WriteString(frame.TextContent);
WritePadding(_binaryWriter, 8);
break;
+ case RenderTreeFrameType.Markup:
+ WriteString(frame.MarkupContent);
+ WritePadding(_binaryWriter, 8);
+ break;
default:
throw new ArgumentException($"Unsupported frame type: {frame.FrameType}");
}
diff --git a/src/Microsoft.AspNetCore.Blazor/MarkupString.cs b/src/Microsoft.AspNetCore.Blazor/MarkupString.cs
new file mode 100644
index 0000000000..c956224647
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor/MarkupString.cs
@@ -0,0 +1,36 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Blazor
+{
+ ///
+ /// A string value that can be rendered as markup such as HTML.
+ ///
+ public struct MarkupString
+ {
+ ///
+ /// Constructs an instance of .
+ ///
+ /// The value for the new instance.
+ public MarkupString(string value)
+ {
+ Value = value;
+ }
+
+ ///
+ /// Gets the value of the .
+ ///
+ public string Value { get; }
+
+ ///
+ /// Casts a to a .
+ ///
+ /// The value.
+ public static explicit operator MarkupString(string value)
+ => new MarkupString(value);
+
+ ///
+ public override string ToString()
+ => Value ?? string.Empty;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs
index 23426dc10d..89dad2fd3a 100644
--- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs
+++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@@ -67,6 +67,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
entry = entry.WithElementSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
}
+ ///
+ /// Appends a frame representing markup content.
+ ///
+ /// An integer that represents the position of the instruction in the source code.
+ /// Content for the new markup frame.
+ public void AddMarkupContent(int sequence, string markupContent)
+ => Append(RenderTreeFrame.Markup(sequence, markupContent ?? string.Empty));
+
///
/// Appends a frame representing text content.
///
@@ -94,6 +102,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
}
}
+ ///
+ /// Appends a frame representing markup content.
+ ///
+ /// An integer that represents the position of the instruction in the source code.
+ /// Content for the new markup frame.
+ public void AddContent(int sequence, MarkupString markupContent)
+ => AddMarkupContent(sequence, markupContent.Value);
+
///
/// Appends a frame representing text content.
///
diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs
index 3ef39edb34..917ec59b41 100644
--- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs
+++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@@ -335,6 +335,19 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
break;
}
+ case RenderTreeFrameType.Markup:
+ {
+ var oldMarkup = oldFrame.MarkupContent;
+ var newMarkup = newFrame.MarkupContent;
+ if (!string.Equals(oldMarkup, newMarkup, StringComparison.Ordinal))
+ {
+ var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
+ diffContext.Edits.Append(RenderTreeEdit.UpdateMarkup(diffContext.SiblingIndex, referenceFrameIndex));
+ }
+ diffContext.SiblingIndex++;
+ break;
+ }
+
case RenderTreeFrameType.Element:
{
var oldElementName = oldFrame.ElementName;
@@ -494,6 +507,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
break;
}
case RenderTreeFrameType.Text:
+ case RenderTreeFrameType.Markup:
{
var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex));
@@ -510,6 +524,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
InitializeNewComponentReferenceCaptureFrame(ref diffContext, ref newFrame);
break;
}
+ default:
+ throw new NotImplementedException($"Unexpected frame type during {nameof(InsertNewFrame)}: {newFrame.FrameType}");
}
}
@@ -548,10 +564,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
break;
}
case RenderTreeFrameType.Text:
+ case RenderTreeFrameType.Markup:
{
diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex));
break;
}
+ default:
+ throw new NotImplementedException($"Unexpected frame type during {nameof(RemoveOldFrame)}: {oldFrame.FrameType}");
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEdit.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEdit.cs
index ffefbd510a..9ea6ded635 100644
--- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEdit.cs
+++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEdit.cs
@@ -1,6 +1,8 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
+
namespace Microsoft.AspNetCore.Blazor.RenderTree
{
///
@@ -65,6 +67,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
internal static RenderTreeEdit UpdateText(int siblingIndex, int referenceFrameIndex)
=> new RenderTreeEdit(RenderTreeEditType.UpdateText, siblingIndex, referenceFrameIndex);
+ internal static RenderTreeEdit UpdateMarkup(int siblingIndex, int referenceFrameIndex)
+ => new RenderTreeEdit(RenderTreeEditType.UpdateMarkup, siblingIndex, referenceFrameIndex);
+
internal static RenderTreeEdit SetAttribute(int siblingIndex, int referenceFrameIndex)
=> new RenderTreeEdit(RenderTreeEditType.SetAttribute, siblingIndex, referenceFrameIndex);
diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEditType.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEditType.cs
index 7fcec8b7ca..7a28a78bd4 100644
--- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEditType.cs
+++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEditType.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Blazor.RenderTree
@@ -45,5 +45,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// edit position should move back to the parent frame.
///
StepOut = 7,
+
+ ///
+ /// Indicates that the markup content of the specified frame (which must be a markup frame)
+ /// should be updated.
+ ///
+ UpdateMarkup = 8,
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs
index 9fc1ea870e..3b89d16ba5 100644
--- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs
+++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@@ -167,6 +167,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
///
[FieldOffset(16)] public readonly Action