Add HTML Block rewriting (#1146)

* Add HTML Block rewriter

* Baseline updates

* test gaps

* Update some unit tests to represent same behavior as before

* Define Markup frame type. Tests for rendering markup frames into render tree.

* Support markup frames during diffing (retain, insert, update, delete)

* Support markup blocks on WebAssembly

* Support rendering markup frames with server-side execution too

* Support markup blocks with multiple top-level nodes. Support updating markup dynamically.

* Define MarkupString type as a way to insert dynamic markup without needing custom RenderFragment code

* Remove comment

* CR: Better null value handling
This commit is contained in:
Ryan Nowak 2018-07-23 10:18:07 -07:00 committed by Steve Sanderson
parent 17b55b983a
commit 8f072a0711
54 changed files with 1121 additions and 139 deletions

View File

@ -6,6 +6,8 @@ import { EventForDotNet, UIEventArgs } from './EventForDotNet';
import { LogicalElement, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement } from './LogicalElements';
import { applyCaptureIdToElement } from './ElementReferenceCapture';
const selectValuePropname = '_blazorSelectValue';
const sharedTemplateElemForParsing = document.createElement('template');
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
let raiseEventMethod: MethodHandle;
let renderComponentMethod: MethodHandle;
@ -113,6 +115,14 @@ export class BrowserRenderer {
}
break;
}
case EditType.updateMarkup: {
const frameIndex = editReader.newTreeIndex(edit);
const frame = batch.referenceFramesEntry(referenceFrames, frameIndex);
const siblingIndex = editReader.siblingIndex(edit);
removeLogicalChild(parent, childIndexAtCurrentDepth + siblingIndex);
this.insertMarkup(batch, parent, childIndexAtCurrentDepth + siblingIndex, frame);
break;
}
case EditType.stepIn: {
const siblingIndex = editReader.siblingIndex(edit);
parent = getLogicalChild(parent, childIndexAtCurrentDepth + siblingIndex);
@ -158,6 +168,9 @@ export class BrowserRenderer {
} else {
throw new Error('Reference capture frames can only be children of element frames.');
}
case FrameType.markup:
this.insertMarkup(batch, parent, childIndex, frame);
return 1;
default:
const unknownType: never = frameType; // Compile-time verification that the switch was exhaustive
throw new Error(`Unknown frame type: ${unknownType}`);
@ -203,6 +216,17 @@ export class BrowserRenderer {
insertLogicalChild(newTextNode, parent, childIndex);
}
private insertMarkup(batch: RenderBatch, parent: LogicalElement, childIndex: number, markupFrame: RenderTreeFrame) {
const markupContainer = createAndInsertLogicalContainer(parent, childIndex);
const markupContent = batch.frameReader.markupContent(markupFrame);
const parsedMarkup = parseMarkup(markupContent, isSvgElement(parent));
let logicalSiblingIndex = 0;
while (parsedMarkup.firstChild) {
insertLogicalChild(parsedMarkup.firstChild, markupContainer, logicalSiblingIndex++);
}
}
private applyAttribute(batch: RenderBatch, componentId: number, toDomElement: Element, attributeFrame: RenderTreeFrame) {
const frameReader = batch.frameReader;
const attributeName = frameReader.attributeName(attributeFrame)!;
@ -305,6 +329,16 @@ export class BrowserRenderer {
}
}
function parseMarkup(markup: string, isSvg: boolean) {
if (isSvg) {
sharedSvgElemForParsing.innerHTML = markup || ' ';
return sharedSvgElemForParsing;
} else {
sharedTemplateElemForParsing.innerHTML = markup || ' ';
return sharedTemplateElemForParsing.content;
}
}
function countDescendantFrames(batch: RenderBatch, frame: RenderTreeFrame): number {
const frameReader = batch.frameReader;
switch (frameReader.frameType(frame)) {

View File

@ -134,6 +134,11 @@ class OutOfProcessRenderTreeFrameReader implements RenderTreeFrameReader {
return this.stringReader.readString(stringIndex);
}
markupContent(frame: RenderTreeFrame) {
const stringIndex = readInt32LE(this.batchDataUint8, frame as any + 4); // 2nd int
return this.stringReader.readString(stringIndex)!;
}
attributeName(frame: RenderTreeFrame) {
const stringIndex = readInt32LE(this.batchDataUint8, frame as any + 4); // 2nd int
return this.stringReader.readString(stringIndex);

View File

@ -47,6 +47,7 @@ export interface RenderTreeFrameReader {
componentId(frame: RenderTreeFrame): number;
elementName(frame: RenderTreeFrame): string | null;
textContent(frame: RenderTreeFrame): string | null;
markupContent(frame: RenderTreeFrame): string;
attributeName(frame: RenderTreeFrame): string | null;
attributeValue(frame: RenderTreeFrame): string | null;
attributeEventHandlerId(frame: RenderTreeFrame): number;
@ -69,6 +70,7 @@ export enum EditType {
updateText = 5,
stepIn = 6,
stepOut = 7,
updateMarkup = 8,
}
export enum FrameType {
@ -79,4 +81,5 @@ export enum FrameType {
component = 4,
region = 5,
elementReferenceCapture = 6,
markup = 8,
}

View File

@ -81,6 +81,7 @@ const frameReader = {
componentId: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 12),
elementName: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
textContent: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
markupContent: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16)!,
attributeName: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
attributeValue: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 24),
attributeEventHandlerId: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 8),

View File

@ -18,6 +18,21 @@ namespace Microsoft.AspNetCore.Blazor.Razor
private readonly static string DesignTimeVariable = "__o";
public override void WriteHtmlBlock(CodeRenderingContext context, HtmlBlockIntermediateNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
// Do nothing
}
public override void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node)
{
if (context == null)

View File

@ -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;
@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
builder.Features.Add(new EventHandlerLoweringPass());
builder.Features.Add(new RefLoweringPass());
builder.Features.Add(new BindLoweringPass());
builder.Features.Add(new HtmlBlockPass());
builder.Features.Add(new ComponentTagHelperDescriptorProvider());
builder.Features.Add(new BindTagHelperDescriptorProvider());

View File

@ -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;
@ -18,6 +18,8 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public abstract void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node);
public abstract void WriteHtmlBlock(CodeRenderingContext context, HtmlBlockIntermediateNode node);
public abstract void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node);
public sealed override void BeginWriterScope(CodeRenderingContext context, string writer)

View File

@ -148,6 +148,26 @@ namespace Microsoft.AspNetCore.Blazor.Razor
}
}
public override void WriteHtmlBlock(CodeRenderingContext context, HtmlBlockIntermediateNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
context.CodeWriter
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddMarkupContent)}")
.Write((_sourceSequence++).ToString())
.WriteParameterSeparator()
.WriteStringLiteral(node.Content)
.WriteEndMethodInvocation();
}
public override void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node)
{
if (context == null)

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{
// Per the HTML spec, the following elements are inherently self-closing
// For example, <img> is the same as <img /> (and therefore it cannot contain descendants)
private readonly static HashSet<string> VoidElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
public readonly static HashSet<string> VoidElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr",
};

View File

@ -0,0 +1,46 @@
// 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;
using System.Diagnostics;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
internal class HtmlBlockIntermediateNode : ExtensionIntermediateNode
{
public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection();
public string Content { get; set; }
public override void Accept(IntermediateNodeVisitor visitor)
{
if (visitor == null)
{
throw new ArgumentNullException(nameof(visitor));
}
AcceptExtensionNode<HtmlBlockIntermediateNode>(this, visitor);
}
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
{
if (target == null)
{
throw new ArgumentNullException(nameof(target));
}
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var writer = (BlazorNodeWriter)context.NodeWriter;
writer.WriteHtmlBlock(context, this);
}
private string DebuggerDisplay => Content;
}
}

View File

@ -0,0 +1,267 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
// Rewrites contiguous subtrees of HTML into a special node type to reduce the
// size of the Render tree.
//
// Does not preserve insigificant details of the HTML, like tag closing style
// or quote style.
internal class HtmlBlockPass : IntermediateNodePassBase, IRazorOptimizationPass
{
// Runs LATE because we want to destroy structure.
public override int Order => 10000;
protected override void ExecuteCore(
RazorCodeDocument codeDocument,
DocumentIntermediateNode documentNode)
{
if (documentNode.Options.DesignTime)
{
// Nothing to do during design time.
return;
}
var findVisitor = new FindHtmlTreeVisitor();
findVisitor.Visit(documentNode);
var trees = findVisitor.Trees;
var rewriteVisitor = new RewriteVisitor(trees);
while (trees.Count > 0)
{
// Walk backwards since we did a postorder traversal.
var reference = trees[trees.Count - 1];
// Forcibly remove a node to prevent infinite loops.
trees.RemoveAt(trees.Count - 1);
rewriteVisitor.Visit(reference.Node);
reference.Replace(new HtmlBlockIntermediateNode()
{
Content = rewriteVisitor.Builder.ToString(),
});
rewriteVisitor.Builder.Clear();
}
}
// Finds HTML-blocks using a postorder traversal. We store nodes in an
// ordered list so we can avoid redundant rewrites.
//
// Consider a case like:
// <div>
// <a href="...">click me</a>
// </div>
//
// 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<HtmlElementIntermediateNode>
{
private bool _foundNonHtml;
public List<IntermediateNodeReference> Trees { get; } = new List<IntermediateNodeReference>();
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<HtmlElementIntermediateNode>
{
private readonly List<IntermediateNodeReference> _trees;
public RewriteVisitor(List<IntermediateNodeReference> 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);
}
}
}
}

View File

@ -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}");
}

View File

@ -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
{
/// <summary>
/// A string value that can be rendered as markup such as HTML.
/// </summary>
public struct MarkupString
{
/// <summary>
/// Constructs an instance of <see cref="MarkupString"/>.
/// </summary>
/// <param name="value">The value for the new instance.</param>
public MarkupString(string value)
{
Value = value;
}
/// <summary>
/// Gets the value of the <see cref="MarkupString"/>.
/// </summary>
public string Value { get; }
/// <summary>
/// Casts a <see cref="string"/> to a <see cref="MarkupString"/>.
/// </summary>
/// <param name="value">The <see cref="string"/> value.</param>
public static explicit operator MarkupString(string value)
=> new MarkupString(value);
/// <inheritdoc />
public override string ToString()
=> Value ?? string.Empty;
}
}

View File

@ -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);
}
/// <summary>
/// Appends a frame representing markup content.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="markupContent">Content for the new markup frame.</param>
public void AddMarkupContent(int sequence, string markupContent)
=> Append(RenderTreeFrame.Markup(sequence, markupContent ?? string.Empty));
/// <summary>
/// Appends a frame representing text content.
/// </summary>
@ -94,6 +102,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
}
}
/// <summary>
/// Appends a frame representing markup content.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="markupContent">Content for the new markup frame.</param>
public void AddContent(int sequence, MarkupString markupContent)
=> AddMarkupContent(sequence, markupContent.Value);
/// <summary>
/// Appends a frame representing text content.
/// </summary>

View File

@ -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}");
}
}

View File

@ -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
{
/// <summary>
@ -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);

View File

@ -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.
/// </summary>
StepOut = 7,
/// <summary>
/// Indicates that the markup content of the specified frame (which must be a markup frame)
/// should be updated.
/// </summary>
UpdateMarkup = 8,
}
}

View File

@ -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
/// </summary>
[FieldOffset(16)] public readonly Action<object> ComponentReferenceCaptureAction;
// --------------------------------------------------------------------------------
// RenderTreeFrameType.Markup
// --------------------------------------------------------------------------------
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Markup"/>,
/// gets the content of the markup frame. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(16)] public readonly string MarkupContent;
private RenderTreeFrame(int sequence, string elementName, int elementSubtreeLength)
: this()
{
@ -245,12 +255,25 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
ComponentReferenceCaptureParentFrameIndex = parentFrameIndex;
}
// If we need further constructors whose signatures clash with the patterns above,
// we can add extra args to this general-purpose one.
private RenderTreeFrame(int sequence, RenderTreeFrameType frameType, string markupContent)
: this()
{
FrameType = frameType;
Sequence = sequence;
MarkupContent = markupContent;
}
internal static RenderTreeFrame Element(int sequence, string elementName)
=> new RenderTreeFrame(sequence, elementName: elementName, elementSubtreeLength: 0);
internal static RenderTreeFrame Text(int sequence, string textContent)
=> new RenderTreeFrame(sequence, textContent: textContent);
internal static RenderTreeFrame Markup(int sequence, string markupContent)
=> new RenderTreeFrame(sequence, RenderTreeFrameType.Markup, markupContent);
internal static RenderTreeFrame Attribute(int sequence, string name, MulticastDelegate value)
=> new RenderTreeFrame(sequence, attributeName: name, attributeValue: value);

View File

@ -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,10 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// Represents an instruction to capture or update a reference to the parent component.
/// </summary>
ComponentReferenceCapture = 7,
/// <summary>
/// Represents a block of markup content.
/// </summary>
Markup = 8,
}
}

View File

@ -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.Shared
@ -57,6 +57,8 @@ namespace Microsoft.AspNetCore.Blazor.Shared
public static readonly string CloseComponent = nameof(CloseComponent);
public static readonly string AddMarkupContent = nameof(AddMarkupContent);
public static readonly string AddContent = nameof(AddContent);
public static readonly string AddAttribute = nameof(AddAttribute);

View File

@ -339,7 +339,7 @@ namespace Test
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent MyAttr=""abc"">Some text<some-child a='1'>Nested text</some-child></MyComponent>");
<MyComponent MyAttr=""abc"">Some text<some-child a='1'>Nested text @(""Hello"")</some-child></MyComponent>");
// Act
var frames = GetRenderTree(component);
@ -356,9 +356,10 @@ namespace Test
Assert.Collection(
childFrames,
frame => AssertFrame.Text(frame, "Some text", 3),
frame => AssertFrame.Element(frame, "some-child", 3, 4),
frame => AssertFrame.Element(frame, "some-child", 4, 4),
frame => AssertFrame.Attribute(frame, "a", "1", 5),
frame => AssertFrame.Text(frame, "Nested text", 6));
frame => AssertFrame.Text(frame, "Nested text ", 6),
frame => AssertFrame.Text(frame, "Hello", 7));
}
[Fact]

View File

@ -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;
@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
public class IntermediateNodeWriter :
IntermediateNodeVisitor,
IExtensionIntermediateNodeVisitor<HtmlElementIntermediateNode>,
IExtensionIntermediateNodeVisitor<HtmlBlockIntermediateNode>,
IExtensionIntermediateNodeVisitor<ComponentExtensionNode>,
IExtensionIntermediateNodeVisitor<ComponentAttributeExtensionNode>,
IExtensionIntermediateNodeVisitor<RouteAttributeExtensionNode>,
@ -269,6 +270,11 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
WriteContentNode(node, node.TagName);
}
void IExtensionIntermediateNodeVisitor<HtmlBlockIntermediateNode>.VisitExtension(HtmlBlockIntermediateNode node)
{
WriteContentNode(node, node.Content);
}
void IExtensionIntermediateNodeVisitor<ComponentExtensionNode>.VisitExtension(ComponentExtensionNode node)
{
WriteContentNode(node, node.TagName, node.TypeName);

View File

@ -1,12 +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;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Layouts;
using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Xunit;
@ -74,19 +70,81 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
}
[Fact]
public void SupportsElements()
public void SupportsElementsWithDynamicContent()
{
// Arrange/Act
var component = CompileToComponent("<myelem>Hello @(\"there\")</myelem>");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "myelem", 3, 0),
frame => AssertFrame.Text(frame, "Hello ", 1),
frame => AssertFrame.Text(frame, "there", 2));
}
[Fact]
public void SupportsElementsAsStaticBlock()
{
// Arrange/Act
var component = CompileToComponent("<myelem>Hello</myelem>");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "myelem", 2, 0),
frame => AssertFrame.Text(frame, "Hello", 1));
frame => AssertFrame.Markup(frame, "<myelem>Hello</myelem>", 0));
}
[Fact]
public void SupportsSelfClosingElements()
public void CreatesSeparateMarkupFrameForEachTopLevelStaticElement()
{
// The JavaScript-side rendering code does not rely on this behavior. It supports
// inserting markup frames with arbitrary markup (e.g., multiple top-level elements
// or none). This test exists only as an observation of the current behavior rather
// than a promise that we never want to change it.
// Arrange/Act
var component = CompileToComponent(
"<root>@(\"Hi\") <child1>a</child1> <child2><another>b</another></child2> </root>");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "root", 7, 0),
frame => AssertFrame.Text(frame, "Hi", 1),
frame => AssertFrame.Text(frame, " ", 2),
frame => AssertFrame.Markup(frame, "<child1>a</child1>", 3),
frame => AssertFrame.Text(frame, " ", 4),
frame => AssertFrame.Markup(frame, "<child2><another>b</another></child2>", 5),
frame => AssertFrame.Text(frame, " ", 6));
}
[Fact]
public void RendersMarkupStringAsMarkupFrame()
{
// Arrange/Act
var component = CompileToComponent(
"@{ var someMarkup = new MarkupString(\"<div>Hello</div>\"); }"
+ "<p>@someMarkup</p>");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "p", 2, 0),
frame => AssertFrame.Markup(frame, "<div>Hello</div>", 1));
}
[Fact]
public void SupportsSelfClosingElementsWithDynamicContent()
{
// Arrange/Act
var component = CompileToComponent("Some text so elem isn't at position 0 <myelem myattr=@(\"val\") />");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0),
frame => AssertFrame.Element(frame, "myelem", 2, 1),
frame => AssertFrame.Attribute(frame, "myattr", "val", 2));
}
[Fact]
public void SupportsSelfClosingElementsAsStaticBlock()
{
// Arrange/Act
var component = CompileToComponent("Some text so elem isn't at position 0 <myelem />");
@ -94,7 +152,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0),
frame => AssertFrame.Element(frame, "myelem", 1, 1));
frame => AssertFrame.Markup(frame, "<myelem/>", 1));
}
[Fact]
@ -106,7 +164,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0),
frame => AssertFrame.Element(frame, "img", 1, 1));
frame => AssertFrame.Markup(frame, "<img>", 1));
}
[Fact]
@ -126,13 +184,14 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public void SupportsAttributesWithLiteralValues()
{
// Arrange/Act
var component = CompileToComponent("<elem attrib-one=\"Value 1\" a2='v2' />");
var component = CompileToComponent("<elem attrib-one=\"Value 1\" a2='v2'>@(\"Hello\")</elem>");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "elem", 3, 0),
frame => AssertFrame.Element(frame, "elem", 4, 0),
frame => AssertFrame.Attribute(frame, "attrib-one", "Value 1", 1),
frame => AssertFrame.Attribute(frame, "a2", "v2", 2));
frame => AssertFrame.Attribute(frame, "a2", "v2", 2),
frame => AssertFrame.Text(frame, "Hello", 3));
}
[Fact]
@ -222,33 +281,21 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
}
[Fact]
public void SupportsDataDashAttributesWithLiteralValues()
{
// Arrange/Act
var component = CompileToComponent(
"<elem data-abc=\"Hello\" />");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "elem", 2, 0),
frame => AssertFrame.Attribute(frame, "data-abc", "Hello", 1));
}
[Fact]
public void SupportsDataDashAttributesWithCSharpExpressionValues()
public void SupportsDataDashAttributes()
{
// Arrange/Act
var component = CompileToComponent(@"
@{
var myValue = ""My string"";
var myValue = ""Expression value"";
}
<elem data-abc=""@myValue"" />");
<elem data-abc=""Literal value"" data-def=""@myValue"" />");
// Assert
Assert.Collection(
GetRenderTree(component),
frame => AssertFrame.Element(frame, "elem", 2, 0),
frame => AssertFrame.Attribute(frame, "data-abc", "My string", 1));
frame => AssertFrame.Element(frame, "elem", 3, 0),
frame => AssertFrame.Attribute(frame, "data-abc", "Literal value", 1),
frame => AssertFrame.Attribute(frame, "data-def", "Expression value", 2));
}
[Fact]

View File

@ -19,10 +19,7 @@ namespace Test
builder.AddAttribute(1, "MyAttr", "abc");
builder.AddAttribute(2, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
builder2.AddContent(3, "Some text");
builder2.OpenElement(4, "some-child");
builder2.AddAttribute(5, "a", "1");
builder2.AddContent(6, "Nested text");
builder2.CloseElement();
builder2.AddMarkupContent(4, "<some-child a=\"1\">Nested text</some-child>");
}
));
builder.CloseComponent();

View File

@ -13,12 +13,7 @@ Document -
ComponentExtensionNode - (31:1,0 [91] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - Test.MyComponent
HtmlContent - (57:1,26 [9] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (57:1,26 [9] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Some text
HtmlElement - (66:1,35 [42] x:\dir\subdir\Test\TestComponent.cshtml) - some-child
HtmlAttribute - - -
HtmlAttributeValue - -
IntermediateToken - - Html - 1
HtmlContent - (84:1,53 [11] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (84:1,53 [11] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Nested text
HtmlBlock - - <some-child a="1">Nested text</some-child>
ComponentAttributeExtensionNode - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - MyAttr - MyAttr
HtmlContent - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - Html - abc

View File

@ -16,12 +16,7 @@ namespace Test
{
base.BuildRenderTree(builder);
builder.OpenComponent<Test.MyComponent>(0);
builder.AddAttribute(1, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
builder2.OpenElement(2, "child");
builder2.AddContent(3, "hello");
builder2.CloseElement();
}
));
builder.AddMarkupContent(1, "<child>hello</child>");
builder.CloseComponent();
}
#pragma warning restore 1998

View File

@ -11,6 +11,4 @@ Document -
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
ComponentExtensionNode - (31:1,0 [47] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - Test.MyComponent
HtmlElement - (44:1,13 [20] x:\dir\subdir\Test\TestComponent.cshtml) - child
HtmlContent - (51:1,20 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (51:1,20 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - hello
HtmlBlock - - <child>hello</child>

View File

@ -19,13 +19,11 @@ namespace Test
builder.AddAttribute(1, "SomeProp", "val");
builder.AddAttribute(2, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
builder2.AddContent(3, "\n Some ");
builder2.OpenElement(4, "el");
builder2.AddContent(5, "further");
builder2.CloseElement();
builder2.AddContent(6, " content\n");
builder2.AddMarkupContent(4, "<el>further</el>");
builder2.AddContent(5, " content\n");
}
));
builder.AddComponentReferenceCapture(7, (__value) => {
builder.AddComponentReferenceCapture(6, (__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
HtmlElement - (87:2,9 [16] x:\dir\subdir\Test\TestComponent.cshtml) - el
HtmlContent - (91:2,13 [7] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (91:2,13 [7] x:\dir\subdir\Test\TestComponent.cshtml) - Html - further
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
RefExtensionNode - (49:1,18 [10] x:\dir\subdir\Test\TestComponent.cshtml) - myInstance - Test.MyComponent

View File

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

View File

@ -17,9 +17,7 @@ namespace Test
base.BuildRenderTree(builder);
builder.AddContent(0, "My value");
builder.AddContent(1, "\n\n");
builder.OpenElement(2, "h1");
builder.AddContent(3, "Hello");
builder.CloseElement();
builder.AddMarkupContent(2, "<h1>Hello</h1>");
}
#pragma warning restore 1998
}

View File

@ -14,6 +14,4 @@ Document -
IntermediateToken - (2:0,2 [10] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - "My value"
HtmlContent - (13:0,13 [4] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (13:0,13 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n
HtmlElement - (17:2,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
HtmlContent - (21:2,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (21:2,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
HtmlBlock - - <h1>Hello</h1>

View File

@ -18,9 +18,7 @@ namespace Test
builder.OpenComponent<Test.SomeOtherComponent>(0);
builder.CloseComponent();
builder.AddContent(1, "\n\n");
builder.OpenElement(2, "h1");
builder.AddContent(3, "Hello");
builder.CloseElement();
builder.AddMarkupContent(2, "<h1>Hello</h1>");
}
#pragma warning restore 1998
}

View File

@ -13,6 +13,4 @@ Document -
ComponentExtensionNode - (36:2,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - SomeOtherComponent - Test.SomeOtherComponent
HtmlContent - (58:2,22 [4] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (58:2,22 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n
HtmlElement - (62:4,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
HtmlContent - (66:4,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (66:4,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
HtmlBlock - - <h1>Hello</h1>

View File

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

View File

@ -10,6 +10,4 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlElement - (17:2,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
HtmlContent - (21:2,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (21:2,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
HtmlBlock - - <h1>Hello</h1>

View File

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

View File

@ -11,9 +11,7 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlElement - (44:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - h1
HtmlContent - (48:3,4 [13] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (48:3,4 [13] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello, world!
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
ComponentExtensionNode - (98:7,0 [23] x:\dir\subdir\Test\TestComponent.cshtml) - SurveyPrompt - Test.SurveyPrompt

View File

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

View File

@ -11,9 +11,7 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlElement - (44:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - h1
HtmlContent - (48:3,4 [13] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (48:3,4 [13] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello, world!
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
ComponentExtensionNode - (98:7,0 [41] x:\dir\subdir\Test\TestComponent.cshtml) - SurveyPrompt - Test.SurveyPrompt

View File

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

View File

@ -10,9 +10,7 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlElement - (0:0,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
HtmlContent - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
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
CSharpExpression - (20:2,2 [10] x:\dir\subdir\Test\TestComponent.cshtml)

View File

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

View File

@ -10,9 +10,7 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlElement - (31:1,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
HtmlContent - (35:1,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (35:1,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
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
ComponentExtensionNode - (49:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - SomeOtherComponent - Test.SomeOtherComponent

View File

@ -16,9 +16,7 @@ namespace Test
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenElement(0, "h1");
builder.AddContent(1, "Hello");
builder.CloseElement();
builder.AddMarkupContent(0, "<h1>Hello</h1>");
}
#pragma warning restore 1998
}

View File

@ -11,6 +11,4 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlElement - (0:0,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
HtmlContent - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
HtmlBlock - - <h1>Hello</h1>

View File

@ -406,6 +406,42 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
Assert.Equal("Value set after render", inputElement.GetAttribute("value"));
}
[Fact]
public void CanRenderMarkupBlocks()
{
var appElement = MountTestComponent<MarkupBlockComponent>();
// Static markup
Assert.Equal(
"attributes",
appElement.FindElement(By.CssSelector("p span#attribute-example")).Text);
// Dynamic markup (from a custom RenderFragment)
Assert.Equal(
"[Here is an example. We support multiple-top-level nodes.]",
appElement.FindElement(By.Id("dynamic-markup-block")).Text);
Assert.Equal(
"example",
appElement.FindElement(By.CssSelector("#dynamic-markup-block strong#dynamic-element em")).Text);
// Dynamic markup (from a MarkupString)
Assert.Equal(
"This is a markup string.",
appElement.FindElement(By.ClassName("markup-string-value")).Text);
Assert.Equal(
"markup string",
appElement.FindElement(By.CssSelector(".markup-string-value em")).Text);
// Updating markup blocks
appElement.FindElement(By.TagName("button")).Click();
WaitAssert.Equal(
"[The output was changed completely.]",
() => appElement.FindElement(By.Id("dynamic-markup-block")).Text);
Assert.Equal(
"changed",
appElement.FindElement(By.CssSelector("#dynamic-markup-block span em")).Text);
}
static IAlert SwitchToAlert(IWebDriver driver)
{
try

View File

@ -0,0 +1,287 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Razor
{
public class HtmlBlockPassTest
{
public HtmlBlockPassTest()
{
Pass = new HtmlBlockPass();
Engine = RazorProjectEngine.Create(
BlazorExtensionInitializer.DefaultConfiguration,
RazorProjectFileSystem.Create(Environment.CurrentDirectory),
b =>
{
BlazorExtensionInitializer.Register(b);
b.Features.Remove(b.Features.OfType<HtmlBlockPass>().Single());
}).Engine;
Pass.Engine = Engine;
}
private RazorEngine Engine { get; }
private HtmlBlockPass Pass { get; }
[Fact]
public void Execute_RewritesHtml_Basic()
{
// Arrange
var document = CreateDocument(@"
<html>
<head cool=""beans"">
Hello, World!
</head>
</html>");
var expected = NormalizeContent(@"
<html>
<head cool=""beans"">
Hello, World!
</head>
</html>");
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()
{
// Arrange
var document = CreateDocument(@"
<html>
<head cool=""beans"" csharp=""@yes"" mixed=""hi @there"">
<div>foo</div>
</head>
</html>");
var expected = NormalizeContent(@"<div>foo</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_CSharpInBody()
{
// Arrange
var document = CreateDocument(@"
<html>
<head cool=""beans"">
<div>@foo</div>
<div>rewriteme</div>
<div>@bar</div>
</head>
</html>");
var expected = NormalizeContent(@"<div>rewriteme</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_SelfClosing()
{
// Arrange
var document = CreateDocument(@"<a href=""...""></a>");
var expected = NormalizeContent(@"<a href=""...""/>");
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_Void()
{
// Arrange
var document = CreateDocument(@"<link rel=""..."" href=""...""/>");
var expected = NormalizeContent(@"<link rel=""..."" href=""..."">");
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_CannotRewriteHtml_CSharpInCode()
{
// Arrange
var document = CreateDocument(@"
<html>
@if (some_bool)
{
<head cool=""beans"">
@hello
</head>
}
</html>");
var documentNode = Lower(document);
// Act
Pass.Execute(document, documentNode);
// Assert
Assert.Empty(documentNode.FindDescendantNodes<HtmlBlockIntermediateNode>());
}
[Fact]
public void Execute_CannotRewriteHtml_Script()
{
// Arrange
var document = CreateDocument(@"
<html>
@if (some_bool)
{
<head cool=""beans"">
<script>...</script>
</head>
}
</html>");
var documentNode = Lower(document);
// Act
Pass.Execute(document, documentNode);
// Assert
Assert.Empty(documentNode.FindDescendantNodes<HtmlBlockIntermediateNode>());
}
// The unclosed tag will have errors, so we won't rewrite it or its parent.
[Fact]
public void Execute_CannotRewriteHtml_Errors()
{
// Arrange
var document = CreateDocument(@"
<html>
<a href=""..."">
</html>");
var documentNode = Lower(document);
// Act
Pass.Execute(document, documentNode);
// Assert
Assert.Empty(documentNode.FindDescendantNodes<HtmlBlockIntermediateNode>());
}
[Fact]
public void Execute_RewritesHtml_MismatchedClosingTag()
{
// Arrange
var document = CreateDocument(@"
<html>
<div>
<div>rewriteme</div>
</span>
</html>");
var expected = NormalizeContent(@"<div>rewriteme</div>");
var documentNode = Lower(document);
// Act
Pass.Execute(document, documentNode);
// Assert
var block = documentNode.FindDescendantNodes<HtmlBlockIntermediateNode>().Single();
Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true);
}
private string NormalizeContent(string content)
{
// Test inputs frequently have leading space for readability.
content = content.TrimStart();
// Normalize newlines since we are testing lengths of things.
content = content.Replace("\r", "");
content = content.Replace("\n", "\r\n");
return content;
}
private RazorCodeDocument CreateDocument(string content)
{
// Normalize newlines since we are testing lengths of things.
content = content.Replace("\r", "");
content = content.Replace("\n", "\r\n");
var source = RazorSourceDocument.Create(content, "test.cshtml");
return RazorCodeDocument.Create(source);
}
private DocumentIntermediateNode Lower(RazorCodeDocument codeDocument)
{
for (var i = 0; i < Engine.Phases.Count; i++)
{
var phase = Engine.Phases[i];
if (phase is IRazorCSharpLoweringPhase)
{
break;
}
phase.Execute(codeDocument);
}
var document = codeDocument.GetDocumentIntermediateNode();
Engine.Features.OfType<ComponentDocumentClassifierPass>().Single().Execute(codeDocument, document);
return document;
}
private class StaticTagHelperFeature : ITagHelperFeature
{
public RazorEngine Engine { get; set; }
public List<TagHelperDescriptor> TagHelpers { get; set; }
public IReadOnlyList<TagHelperDescriptor> GetDescriptors()
{
return TagHelpers;
}
}
}
}

View File

@ -54,6 +54,61 @@ namespace Microsoft.AspNetCore.Blazor.Test
frame => AssertFrame.Text(frame, "Second item"));
}
[Fact]
public void CanAddMarkup()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.OpenElement(0, "some elem");
builder.AddMarkupContent(1, "Blah");
builder.AddMarkupContent(2, string.Empty);
builder.CloseElement();
// Assert
var frames = builder.GetFrames();
Assert.Collection(frames,
frame => AssertFrame.Element(frame, "some elem", 3),
frame => AssertFrame.Markup(frame, "Blah"),
frame => AssertFrame.Markup(frame, string.Empty));
}
[Fact]
public void CanAddMarkupViaMarkupString()
{
// This represents putting @someMarkupString into the component,
// as opposed to calling builder.AddMarkupContent directly.
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act - can use either constructor or cast
builder.AddContent(0, (MarkupString)"Some markup");
builder.AddContent(1, new MarkupString(null));
// Assert
var frames = builder.GetFrames();
Assert.Collection(frames,
frame => AssertFrame.Markup(frame, "Some markup"),
frame => AssertFrame.Markup(frame, string.Empty));
}
[Fact]
public void CanAddNullMarkup()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.AddMarkupContent(0, null);
// Assert
var frames = builder.GetFrames();
Assert.Collection(frames,
frame => AssertFrame.Markup(frame, string.Empty));
}
[Fact]
public void CanAddNonStringValueAsText()
{

View File

@ -295,6 +295,40 @@ namespace Microsoft.AspNetCore.Blazor.Test
});
}
[Fact]
public void RecognizesMarkupChanges()
{
// Arrange
oldTree.AddMarkupContent(1, "preserved");
oldTree.AddMarkupContent(3, "will be updated");
oldTree.AddMarkupContent(4, "will be removed");
newTree.AddMarkupContent(1, "preserved");
newTree.AddMarkupContent(2, "was inserted");
newTree.AddMarkupContent(3, "was updated");
// Act
var (result, referenceFrames) = GetSingleUpdatedComponent();
// Assert
Assert.Collection(result.Edits,
entry =>
{
AssertEdit(entry, RenderTreeEditType.PrependFrame, 1);
Assert.Equal(0, entry.ReferenceFrameIndex);
Assert.Equal("was inserted", referenceFrames[entry.ReferenceFrameIndex].MarkupContent);
},
entry =>
{
AssertEdit(entry, RenderTreeEditType.UpdateMarkup, 2);
Assert.Equal(1, entry.ReferenceFrameIndex);
Assert.Equal("was updated", referenceFrames[entry.ReferenceFrameIndex].MarkupContent);
},
entry =>
{
AssertEdit(entry, RenderTreeEditType.RemoveFrame, 3);
});
}
[Fact]
public void RecognizesElementNameChangesAtSameSequenceNumber()
{

View File

@ -146,6 +146,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
RenderTreeEdit.UpdateText(105, 106),
RenderTreeEdit.StepIn(107),
RenderTreeEdit.StepOut(),
RenderTreeEdit.UpdateMarkup(108, 109),
};
var bytes = Serialize(new RenderBatch(
new ArrayRange<RenderTreeDiff>(new[]
@ -165,14 +166,15 @@ namespace Microsoft.AspNetCore.Blazor.Server
AssertBinaryContents(bytes, 0,
123, // Component ID for diff 0
7, // diff[0].Edits.Count
8, // diff[0].Edits.Count
RenderTreeEditType.PrependFrame, 456, 789, NullStringMarker,
RenderTreeEditType.RemoveFrame, 101, 0, NullStringMarker,
RenderTreeEditType.SetAttribute, 102, 103, NullStringMarker,
RenderTreeEditType.RemoveAttribute, 104, 0, "Some removed attribute",
RenderTreeEditType.UpdateText, 105, 106, NullStringMarker,
RenderTreeEditType.StepIn, 107, 0, NullStringMarker,
RenderTreeEditType.StepOut, 0, 0, NullStringMarker
RenderTreeEditType.StepOut, 0, 0, NullStringMarker,
RenderTreeEditType.UpdateMarkup, 108, 109, NullStringMarker
);
}
@ -198,14 +200,15 @@ namespace Microsoft.AspNetCore.Blazor.Server
RenderTreeFrame.Region(130)
.WithRegionSubtreeLength(1234),
RenderTreeFrame.Text(131, "Some text"),
}, 9),
RenderTreeFrame.Markup(132, "Some markup"),
}, 10),
default,
default));
// Assert
var referenceFramesStartIndex = ReadInt(bytes, bytes.Length - 16);
AssertBinaryContents(bytes, referenceFramesStartIndex,
9, // Number of frames
10, // Number of frames
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0,
RenderTreeFrameType.Attribute, "Attribute with nonstring value", NullStringMarker, 0,
RenderTreeFrameType.Attribute, "Attribute with delegate value", NullStringMarker, 789,
@ -214,7 +217,8 @@ namespace Microsoft.AspNetCore.Blazor.Server
RenderTreeFrameType.Element, 1234, "Some element", 0,
RenderTreeFrameType.ElementReferenceCapture, "my unique ID", 0, 0,
RenderTreeFrameType.Region, 1234, 0, 0,
RenderTreeFrameType.Text, "Some text", 0, 0
RenderTreeFrameType.Text, "Some text", 0, 0,
RenderTreeFrameType.Markup, "Some markup", 0, 0
);
}

View File

@ -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;
@ -26,6 +26,14 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers
AssertFrame.Sequence(frame, sequence);
}
internal static void Markup(RenderTreeFrame frame, string markupContent, int? sequence = null)
{
Assert.Equal(RenderTreeFrameType.Markup, frame.FrameType);
Assert.Equal(markupContent, frame.MarkupContent);
Assert.Equal(0, frame.ElementSubtreeLength);
AssertFrame.Sequence(frame, sequence);
}
public static void Element(RenderTreeFrame frame, string elementName, int subtreeLength, int? sequence = null)
{
Assert.Equal(RenderTreeFrameType.Element, frame.FrameType);

View File

@ -18,6 +18,7 @@
<option value="BasicTestApp.RedTextComponent">Red text</option>
<option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option>
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
<option value="BasicTestApp.MarkupBlockComponent">Markup blocks</option>
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
<option value="BasicTestApp.HttpClientTest.BinaryHttpRequestsComponent">Binary HttpClient tester</option>

View File

@ -0,0 +1,44 @@
@using Microsoft.AspNetCore.Blazor.RenderTree
<h1>Markup blocks</h1>
<p>
This component contains blocks of <em>static</em> HTML markup that will be
represented in the render instructions as single frames.
This includes nested elements with <span id="attribute-example">attributes</span>.
</p>
<h2>Dynamic markup</h2>
<p>It's also possible to emit markup blocks from render fragments:</p>
<div id="dynamic-markup-block">
[@((RenderFragment)EmitMarkupBlock)]
</div>
<button onclick=@(() => { changeOutput = true; })>Change output</button>
<h2>Markup string</h2>
<p>It's also possible to declare a value of a special type that renders as markup:</p>
@((MarkupString)myMarkup)
@functions {
bool changeOutput;
string myMarkup = "<p class='markup-string-value'>This is a <em>markup string</em>.</p>";
void EmitMarkupBlock(RenderTreeBuilder builder)
{
// To show we detect and apply changes to markup blocks
if (!changeOutput)
{
builder.AddMarkupContent(0, "Here is <strong id='dynamic-element'>an <em>example</em>.</strong> We support multiple-top-level nodes.");
}
else
{
builder.AddMarkupContent(0, "<span>The output was <em>changed</em></span> completely.");
}
}
}