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 { LogicalElement, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement } from './LogicalElements';
import { applyCaptureIdToElement } from './ElementReferenceCapture'; import { applyCaptureIdToElement } from './ElementReferenceCapture';
const selectValuePropname = '_blazorSelectValue'; const selectValuePropname = '_blazorSelectValue';
const sharedTemplateElemForParsing = document.createElement('template');
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
let raiseEventMethod: MethodHandle; let raiseEventMethod: MethodHandle;
let renderComponentMethod: MethodHandle; let renderComponentMethod: MethodHandle;
@ -113,6 +115,14 @@ export class BrowserRenderer {
} }
break; 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: { case EditType.stepIn: {
const siblingIndex = editReader.siblingIndex(edit); const siblingIndex = editReader.siblingIndex(edit);
parent = getLogicalChild(parent, childIndexAtCurrentDepth + siblingIndex); parent = getLogicalChild(parent, childIndexAtCurrentDepth + siblingIndex);
@ -158,6 +168,9 @@ export class BrowserRenderer {
} else { } else {
throw new Error('Reference capture frames can only be children of element frames.'); 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: default:
const unknownType: never = frameType; // Compile-time verification that the switch was exhaustive const unknownType: never = frameType; // Compile-time verification that the switch was exhaustive
throw new Error(`Unknown frame type: ${unknownType}`); throw new Error(`Unknown frame type: ${unknownType}`);
@ -203,6 +216,17 @@ export class BrowserRenderer {
insertLogicalChild(newTextNode, parent, childIndex); 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) { private applyAttribute(batch: RenderBatch, componentId: number, toDomElement: Element, attributeFrame: RenderTreeFrame) {
const frameReader = batch.frameReader; const frameReader = batch.frameReader;
const attributeName = frameReader.attributeName(attributeFrame)!; 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 { function countDescendantFrames(batch: RenderBatch, frame: RenderTreeFrame): number {
const frameReader = batch.frameReader; const frameReader = batch.frameReader;
switch (frameReader.frameType(frame)) { switch (frameReader.frameType(frame)) {

View File

@ -134,6 +134,11 @@ class OutOfProcessRenderTreeFrameReader implements RenderTreeFrameReader {
return this.stringReader.readString(stringIndex); 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) { attributeName(frame: RenderTreeFrame) {
const stringIndex = readInt32LE(this.batchDataUint8, frame as any + 4); // 2nd int const stringIndex = readInt32LE(this.batchDataUint8, frame as any + 4); // 2nd int
return this.stringReader.readString(stringIndex); return this.stringReader.readString(stringIndex);

View File

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

View File

@ -81,6 +81,7 @@ const frameReader = {
componentId: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 12), componentId: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 12),
elementName: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16), elementName: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
textContent: (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), attributeName: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
attributeValue: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 24), attributeValue: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 24),
attributeEventHandlerId: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 8), 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"; 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) public override void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node)
{ {
if (context == null) 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -79,6 +79,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
builder.Features.Add(new EventHandlerLoweringPass()); builder.Features.Add(new EventHandlerLoweringPass());
builder.Features.Add(new RefLoweringPass()); builder.Features.Add(new RefLoweringPass());
builder.Features.Add(new BindLoweringPass()); builder.Features.Add(new BindLoweringPass());
builder.Features.Add(new HtmlBlockPass());
builder.Features.Add(new ComponentTagHelperDescriptorProvider()); builder.Features.Add(new ComponentTagHelperDescriptorProvider());
builder.Features.Add(new BindTagHelperDescriptorProvider()); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -18,6 +18,8 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public abstract void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node); 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 abstract void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node);
public sealed override void BeginWriterScope(CodeRenderingContext context, string writer) 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) public override void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node)
{ {
if (context == null) 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 // 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) // 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", "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); WriteString(frame.TextContent);
WritePadding(_binaryWriter, 8); WritePadding(_binaryWriter, 8);
break; break;
case RenderTreeFrameType.Markup:
WriteString(frame.MarkupContent);
WritePadding(_binaryWriter, 8);
break;
default: default:
throw new ArgumentException($"Unsupported frame type: {frame.FrameType}"); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -67,6 +67,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
entry = entry.WithElementSubtreeLength(_entries.Count - indexOfEntryBeingClosed); 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> /// <summary>
/// Appends a frame representing text content. /// Appends a frame representing text content.
/// </summary> /// </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> /// <summary>
/// Appends a frame representing text content. /// Appends a frame representing text content.
/// </summary> /// </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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -335,6 +335,19 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
break; 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: case RenderTreeFrameType.Element:
{ {
var oldElementName = oldFrame.ElementName; var oldElementName = oldFrame.ElementName;
@ -494,6 +507,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
break; break;
} }
case RenderTreeFrameType.Text: case RenderTreeFrameType.Text:
case RenderTreeFrameType.Markup:
{ {
var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame); var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex)); diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex));
@ -510,6 +524,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
InitializeNewComponentReferenceCaptureFrame(ref diffContext, ref newFrame); InitializeNewComponentReferenceCaptureFrame(ref diffContext, ref newFrame);
break; break;
} }
default:
throw new NotImplementedException($"Unexpected frame type during {nameof(InsertNewFrame)}: {newFrame.FrameType}");
} }
} }
@ -548,10 +564,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
break; break;
} }
case RenderTreeFrameType.Text: case RenderTreeFrameType.Text:
case RenderTreeFrameType.Markup:
{ {
diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex)); diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex));
break; 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. // 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 namespace Microsoft.AspNetCore.Blazor.RenderTree
{ {
/// <summary> /// <summary>
@ -65,6 +67,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
internal static RenderTreeEdit UpdateText(int siblingIndex, int referenceFrameIndex) internal static RenderTreeEdit UpdateText(int siblingIndex, int referenceFrameIndex)
=> new RenderTreeEdit(RenderTreeEditType.UpdateText, siblingIndex, 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) internal static RenderTreeEdit SetAttribute(int siblingIndex, int referenceFrameIndex)
=> new RenderTreeEdit(RenderTreeEditType.SetAttribute, siblingIndex, 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Blazor.RenderTree namespace Microsoft.AspNetCore.Blazor.RenderTree
@ -45,5 +45,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// edit position should move back to the parent frame. /// edit position should move back to the parent frame.
/// </summary> /// </summary>
StepOut = 7, 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -167,6 +167,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// </summary> /// </summary>
[FieldOffset(16)] public readonly Action<object> ComponentReferenceCaptureAction; [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) private RenderTreeFrame(int sequence, string elementName, int elementSubtreeLength)
: this() : this()
{ {
@ -245,12 +255,25 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
ComponentReferenceCaptureParentFrameIndex = parentFrameIndex; 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) internal static RenderTreeFrame Element(int sequence, string elementName)
=> new RenderTreeFrame(sequence, elementName: elementName, elementSubtreeLength: 0); => new RenderTreeFrame(sequence, elementName: elementName, elementSubtreeLength: 0);
internal static RenderTreeFrame Text(int sequence, string textContent) internal static RenderTreeFrame Text(int sequence, string textContent)
=> new RenderTreeFrame(sequence, textContent: 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) internal static RenderTreeFrame Attribute(int sequence, string name, MulticastDelegate value)
=> new RenderTreeFrame(sequence, attributeName: name, attributeValue: 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Blazor.RenderTree 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. /// Represents an instruction to capture or update a reference to the parent component.
/// </summary> /// </summary>
ComponentReferenceCapture = 7, 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Blazor.Shared 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 CloseComponent = nameof(CloseComponent);
public static readonly string AddMarkupContent = nameof(AddMarkupContent);
public static readonly string AddContent = nameof(AddContent); public static readonly string AddContent = nameof(AddContent);
public static readonly string AddAttribute = nameof(AddAttribute); public static readonly string AddAttribute = nameof(AddAttribute);

View File

@ -339,7 +339,7 @@ namespace Test
var component = CompileToComponent(@" var component = CompileToComponent(@"
@addTagHelper *, TestAssembly @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 // Act
var frames = GetRenderTree(component); var frames = GetRenderTree(component);
@ -356,9 +356,10 @@ namespace Test
Assert.Collection( Assert.Collection(
childFrames, childFrames,
frame => AssertFrame.Text(frame, "Some text", 3), 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.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] [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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
public class IntermediateNodeWriter : public class IntermediateNodeWriter :
IntermediateNodeVisitor, IntermediateNodeVisitor,
IExtensionIntermediateNodeVisitor<HtmlElementIntermediateNode>, IExtensionIntermediateNodeVisitor<HtmlElementIntermediateNode>,
IExtensionIntermediateNodeVisitor<HtmlBlockIntermediateNode>,
IExtensionIntermediateNodeVisitor<ComponentExtensionNode>, IExtensionIntermediateNodeVisitor<ComponentExtensionNode>,
IExtensionIntermediateNodeVisitor<ComponentAttributeExtensionNode>, IExtensionIntermediateNodeVisitor<ComponentAttributeExtensionNode>,
IExtensionIntermediateNodeVisitor<RouteAttributeExtensionNode>, IExtensionIntermediateNodeVisitor<RouteAttributeExtensionNode>,
@ -269,6 +270,11 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
WriteContentNode(node, node.TagName); WriteContentNode(node, node.TagName);
} }
void IExtensionIntermediateNodeVisitor<HtmlBlockIntermediateNode>.VisitExtension(HtmlBlockIntermediateNode node)
{
WriteContentNode(node, node.Content);
}
void IExtensionIntermediateNodeVisitor<ComponentExtensionNode>.VisitExtension(ComponentExtensionNode node) void IExtensionIntermediateNodeVisitor<ComponentExtensionNode>.VisitExtension(ComponentExtensionNode node)
{ {
WriteContentNode(node, node.TagName, node.TypeName); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic; 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.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Helpers; using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Xunit; using Xunit;
@ -74,19 +70,81 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
} }
[Fact] [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 // Arrange/Act
var component = CompileToComponent("<myelem>Hello</myelem>"); var component = CompileToComponent("<myelem>Hello</myelem>");
// Assert // Assert
Assert.Collection(GetRenderTree(component), Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "myelem", 2, 0), frame => AssertFrame.Markup(frame, "<myelem>Hello</myelem>", 0));
frame => AssertFrame.Text(frame, "Hello", 1));
} }
[Fact] [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 // Arrange/Act
var component = CompileToComponent("Some text so elem isn't at position 0 <myelem />"); 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
Assert.Collection(GetRenderTree(component), Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0), 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] [Fact]
@ -106,7 +164,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Assert // Assert
Assert.Collection(GetRenderTree(component), Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0), 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] [Fact]
@ -126,13 +184,14 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public void SupportsAttributesWithLiteralValues() public void SupportsAttributesWithLiteralValues()
{ {
// Arrange/Act // 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
Assert.Collection(GetRenderTree(component), 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, "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] [Fact]
@ -222,33 +281,21 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
} }
[Fact] [Fact]
public void SupportsDataDashAttributesWithLiteralValues() public void SupportsDataDashAttributes()
{
// 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()
{ {
// Arrange/Act // Arrange/Act
var component = CompileToComponent(@" 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
Assert.Collection( Assert.Collection(
GetRenderTree(component), GetRenderTree(component),
frame => AssertFrame.Element(frame, "elem", 2, 0), frame => AssertFrame.Element(frame, "elem", 3, 0),
frame => AssertFrame.Attribute(frame, "data-abc", "My string", 1)); frame => AssertFrame.Attribute(frame, "data-abc", "Literal value", 1),
frame => AssertFrame.Attribute(frame, "data-def", "Expression value", 2));
} }
[Fact] [Fact]

View File

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

View File

@ -13,12 +13,7 @@ Document -
ComponentExtensionNode - (31:1,0 [91] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - Test.MyComponent 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) 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 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 HtmlBlock - - <some-child a="1">Nested text</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
ComponentAttributeExtensionNode - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - MyAttr - MyAttr ComponentAttributeExtensionNode - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - MyAttr - MyAttr
HtmlContent - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) HtmlContent - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - Html - abc IntermediateToken - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - Html - abc

View File

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

View File

@ -11,6 +11,4 @@ Document -
CSharpCode - CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder); IntermediateToken - - CSharp - base.BuildRenderTree(builder);
ComponentExtensionNode - (31:1,0 [47] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - Test.MyComponent 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 HtmlBlock - - <child>hello</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

View File

@ -19,13 +19,11 @@ namespace Test
builder.AddAttribute(1, "SomeProp", "val"); builder.AddAttribute(1, "SomeProp", "val");
builder.AddAttribute(2, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => { builder.AddAttribute(2, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
builder2.AddContent(3, "\n Some "); builder2.AddContent(3, "\n Some ");
builder2.OpenElement(4, "el"); builder2.AddMarkupContent(4, "<el>further</el>");
builder2.AddContent(5, "further"); builder2.AddContent(5, " content\n");
builder2.CloseElement();
builder2.AddContent(6, " content\n");
} }
)); ));
builder.AddComponentReferenceCapture(7, (__value) => { builder.AddComponentReferenceCapture(6, (__value) => {
#line 2 "x:\dir\subdir\Test\TestComponent.cshtml" #line 2 "x:\dir\subdir\Test\TestComponent.cshtml"
myInstance = (Test.MyComponent)__value; 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 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) 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 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 HtmlBlock - - <el>further</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
HtmlContent - (103:2,25 [10] x:\dir\subdir\Test\TestComponent.cshtml) 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 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 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) Source Location: (49:1,18 [10] x:\dir\subdir\Test\TestComponent.cshtml)
|myInstance| |myInstance|
Generated Location: (1251:30,18 [10] ) Generated Location: (1176:28,18 [10] )
|myInstance| |myInstance|
Source Location: (143:5,12 [44] x:\dir\subdir\Test\TestComponent.cshtml) Source Location: (143:5,12 [44] x:\dir\subdir\Test\TestComponent.cshtml)
| |
private Test.MyComponent myInstance; private Test.MyComponent myInstance;
| |
Generated Location: (1505:40,12 [44] ) Generated Location: (1430:38,12 [44] )
| |
private Test.MyComponent myInstance; private Test.MyComponent myInstance;
| |

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,9 +11,7 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode - CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder); IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlElement - (44:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - h1 HtmlBlock - - <h1>Hello, world!</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!
HtmlContent - (66:3,22 [32] x:\dir\subdir\Test\TestComponent.cshtml) 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 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 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) protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{ {
base.BuildRenderTree(builder); base.BuildRenderTree(builder);
builder.OpenElement(0, "h1"); builder.AddMarkupContent(0, "<h1>Hello, world!</h1>");
builder.AddContent(1, "Hello, world!"); builder.AddContent(1, "\n\nWelcome to your new app.\n\n");
builder.CloseElement(); builder.OpenComponent<Test.SurveyPrompt>(2);
builder.AddContent(2, "\n\nWelcome to your new app.\n\n"); builder.AddAttribute(3, "Title", "<div>Test!</div>");
builder.OpenComponent<Test.SurveyPrompt>(3);
builder.AddAttribute(4, "Title", "<div>Test!</div>");
builder.CloseComponent(); builder.CloseComponent();
} }
#pragma warning restore 1998 #pragma warning restore 1998

View File

@ -11,9 +11,7 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode - CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder); IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlElement - (44:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - h1 HtmlBlock - - <h1>Hello, world!</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!
HtmlContent - (66:3,22 [32] x:\dir\subdir\Test\TestComponent.cshtml) 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 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 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) protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{ {
base.BuildRenderTree(builder); base.BuildRenderTree(builder);
builder.OpenElement(0, "h1"); builder.AddMarkupContent(0, "<h1>Hello</h1>");
builder.AddContent(1, "Hello"); builder.AddContent(1, "\n\n");
builder.CloseElement(); builder.AddContent(2, "My value");
builder.AddContent(2, "\n\n");
builder.AddContent(3, "My value");
} }
#pragma warning restore 1998 #pragma warning restore 1998
} }

View File

@ -10,9 +10,7 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode - CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder); IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlElement - (0:0,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1 HtmlBlock - - <h1>Hello</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
HtmlContent - (14:0,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) 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 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) 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) protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{ {
base.BuildRenderTree(builder); base.BuildRenderTree(builder);
builder.OpenElement(0, "h1"); builder.AddMarkupContent(0, "<h1>Hello</h1>");
builder.AddContent(1, "Hello"); builder.AddContent(1, "\n\n");
builder.CloseElement(); builder.OpenComponent<Test.SomeOtherComponent>(2);
builder.AddContent(2, "\n\n");
builder.OpenComponent<Test.SomeOtherComponent>(3);
builder.CloseComponent(); builder.CloseComponent();
} }
#pragma warning restore 1998 #pragma warning restore 1998

View File

@ -10,9 +10,7 @@ Document -
MethodDeclaration - - protected override - void - BuildRenderTree MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode - CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder); IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlElement - (31:1,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1 HtmlBlock - - <h1>Hello</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
HtmlContent - (45:1,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) 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 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 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) protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{ {
base.BuildRenderTree(builder); base.BuildRenderTree(builder);
builder.OpenElement(0, "h1"); builder.AddMarkupContent(0, "<h1>Hello</h1>");
builder.AddContent(1, "Hello");
builder.CloseElement();
} }
#pragma warning restore 1998 #pragma warning restore 1998
} }

View File

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

View File

@ -406,6 +406,42 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
Assert.Equal("Value set after render", inputElement.GetAttribute("value")); 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) static IAlert SwitchToAlert(IWebDriver driver)
{ {
try 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")); 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] [Fact]
public void CanAddNonStringValueAsText() 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] [Fact]
public void RecognizesElementNameChangesAtSameSequenceNumber() public void RecognizesElementNameChangesAtSameSequenceNumber()
{ {

View File

@ -146,6 +146,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
RenderTreeEdit.UpdateText(105, 106), RenderTreeEdit.UpdateText(105, 106),
RenderTreeEdit.StepIn(107), RenderTreeEdit.StepIn(107),
RenderTreeEdit.StepOut(), RenderTreeEdit.StepOut(),
RenderTreeEdit.UpdateMarkup(108, 109),
}; };
var bytes = Serialize(new RenderBatch( var bytes = Serialize(new RenderBatch(
new ArrayRange<RenderTreeDiff>(new[] new ArrayRange<RenderTreeDiff>(new[]
@ -165,14 +166,15 @@ namespace Microsoft.AspNetCore.Blazor.Server
AssertBinaryContents(bytes, 0, AssertBinaryContents(bytes, 0,
123, // Component ID for diff 0 123, // Component ID for diff 0
7, // diff[0].Edits.Count 8, // diff[0].Edits.Count
RenderTreeEditType.PrependFrame, 456, 789, NullStringMarker, RenderTreeEditType.PrependFrame, 456, 789, NullStringMarker,
RenderTreeEditType.RemoveFrame, 101, 0, NullStringMarker, RenderTreeEditType.RemoveFrame, 101, 0, NullStringMarker,
RenderTreeEditType.SetAttribute, 102, 103, NullStringMarker, RenderTreeEditType.SetAttribute, 102, 103, NullStringMarker,
RenderTreeEditType.RemoveAttribute, 104, 0, "Some removed attribute", RenderTreeEditType.RemoveAttribute, 104, 0, "Some removed attribute",
RenderTreeEditType.UpdateText, 105, 106, NullStringMarker, RenderTreeEditType.UpdateText, 105, 106, NullStringMarker,
RenderTreeEditType.StepIn, 107, 0, 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) RenderTreeFrame.Region(130)
.WithRegionSubtreeLength(1234), .WithRegionSubtreeLength(1234),
RenderTreeFrame.Text(131, "Some text"), RenderTreeFrame.Text(131, "Some text"),
}, 9), RenderTreeFrame.Markup(132, "Some markup"),
}, 10),
default, default,
default)); default));
// Assert // Assert
var referenceFramesStartIndex = ReadInt(bytes, bytes.Length - 16); var referenceFramesStartIndex = ReadInt(bytes, bytes.Length - 16);
AssertBinaryContents(bytes, referenceFramesStartIndex, AssertBinaryContents(bytes, referenceFramesStartIndex,
9, // Number of frames 10, // Number of frames
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0, RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0,
RenderTreeFrameType.Attribute, "Attribute with nonstring value", NullStringMarker, 0, RenderTreeFrameType.Attribute, "Attribute with nonstring value", NullStringMarker, 0,
RenderTreeFrameType.Attribute, "Attribute with delegate value", NullStringMarker, 789, RenderTreeFrameType.Attribute, "Attribute with delegate value", NullStringMarker, 789,
@ -214,7 +217,8 @@ namespace Microsoft.AspNetCore.Blazor.Server
RenderTreeFrameType.Element, 1234, "Some element", 0, RenderTreeFrameType.Element, 1234, "Some element", 0,
RenderTreeFrameType.ElementReferenceCapture, "my unique ID", 0, 0, RenderTreeFrameType.ElementReferenceCapture, "my unique ID", 0, 0,
RenderTreeFrameType.Region, 1234, 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -26,6 +26,14 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers
AssertFrame.Sequence(frame, sequence); 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) public static void Element(RenderTreeFrame frame, string elementName, int subtreeLength, int? sequence = null)
{ {
Assert.Equal(RenderTreeFrameType.Element, frame.FrameType); Assert.Equal(RenderTreeFrameType.Element, frame.FrameType);

View File

@ -18,6 +18,7 @@
<option value="BasicTestApp.RedTextComponent">Red text</option> <option value="BasicTestApp.RedTextComponent">Red text</option>
<option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option> <option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option>
<option value="BasicTestApp.TextOnlyComponent">Plain text</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.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option> <option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
<option value="BasicTestApp.HttpClientTest.BinaryHttpRequestsComponent">Binary 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.");
}
}
}