From 8f072a071112dc24a3501f93368fa3b58f09439c Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 23 Jul 2018 10:18:07 -0700 Subject: [PATCH] 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 --- .../src/Rendering/BrowserRenderer.ts | 34 +++ .../RenderBatch/OutOfProcessRenderBatch.ts | 5 + .../src/Rendering/RenderBatch/RenderBatch.ts | 3 + .../RenderBatch/SharedMemoryRenderBatch.ts | 1 + .../BlazorDesignTimeNodeWriter.cs | 15 + .../BlazorExtensionInitializer.cs | 3 +- .../BlazorNodeWriter.cs | 4 +- .../BlazorRuntimeNodeWriter.cs | 20 ++ .../ComponentDocumentRewritePass.cs | 2 +- .../HtmlBlockIntermediateNode.cs | 46 +++ .../HtmlBlockPass.cs | 267 ++++++++++++++++ .../Circuits/RenderBatchWriter.cs | 4 + .../MarkupString.cs | 36 +++ .../RenderTree/RenderTreeBuilder.cs | 18 +- .../RenderTree/RenderTreeDiffBuilder.cs | 21 +- .../RenderTree/RenderTreeEdit.cs | 7 +- .../RenderTree/RenderTreeEditType.cs | 8 +- .../RenderTree/RenderTreeFrame.cs | 25 +- .../RenderTree/RenderTreeFrameType.cs | 7 +- src/shared/BlazorApi.cs | 4 +- .../ComponentRenderingRazorIntegrationTest.cs | 7 +- .../Razor/IntermediateNodeWriter.cs | 8 +- .../RenderingRazorIntegrationTest.cs | 111 +++++-- .../TestComponent.codegen.cs | 5 +- .../TestComponent.ir.txt | 7 +- .../TestComponent.codegen.cs | 7 +- .../TestComponent.ir.txt | 4 +- .../TestComponent.codegen.cs | 8 +- .../TestComponent.ir.txt | 4 +- .../TestComponent.mappings.txt | 4 +- .../TestComponent.codegen.cs | 4 +- .../TestComponent.ir.txt | 4 +- .../TestComponent.codegen.cs | 4 +- .../TestComponent.ir.txt | 4 +- .../TestComponent.codegen.cs | 4 +- .../TestComponent.ir.txt | 4 +- .../Regression_772/TestComponent.codegen.cs | 10 +- .../Regression_772/TestComponent.ir.txt | 4 +- .../Regression_773/TestComponent.codegen.cs | 10 +- .../Regression_773/TestComponent.ir.txt | 4 +- .../TestComponent.codegen.cs | 8 +- .../TestComponent.ir.txt | 4 +- .../TestComponent.codegen.cs | 8 +- .../TestComponent.ir.txt | 4 +- .../TestComponent.codegen.cs | 4 +- .../TestComponent.ir.txt | 4 +- .../Tests/ComponentRenderingTest.cs | 36 +++ .../HtmlBlockPassTest.cs | 287 ++++++++++++++++++ .../RenderTreeBuilderTest.cs | 55 ++++ .../RenderTreeDiffBuilderTest.cs | 34 +++ .../Circuits/RenderBatchWriterTest.cs | 14 +- test/shared/AssertFrame.cs | 10 +- test/testapps/BasicTestApp/Index.cshtml | 1 + .../BasicTestApp/MarkupBlockComponent.cshtml | 44 +++ 54 files changed, 1121 insertions(+), 139 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/HtmlBlockIntermediateNode.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/HtmlBlockPass.cs create mode 100644 src/Microsoft.AspNetCore.Blazor/MarkupString.cs create mode 100644 test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/HtmlBlockPassTest.cs create mode 100644 test/testapps/BasicTestApp/MarkupBlockComponent.cshtml diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts index 164c63c4c7..0d4b0af817 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts @@ -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)) { diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/OutOfProcessRenderBatch.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/OutOfProcessRenderBatch.ts index 2d8dde08f9..c1951be032 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/OutOfProcessRenderBatch.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/OutOfProcessRenderBatch.ts @@ -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); diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/RenderBatch.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/RenderBatch.ts index 3f95e9f5f7..3faddab664 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/RenderBatch.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/RenderBatch.ts @@ -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, } diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/SharedMemoryRenderBatch.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/SharedMemoryRenderBatch.ts index d6dad6cfbc..5c619d5575 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/SharedMemoryRenderBatch.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderBatch/SharedMemoryRenderBatch.ts @@ -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), diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs index 3ed8d0959c..b6441fd9e7 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs @@ -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) diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs index e69cfa16a1..55ca4908a8 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorExtensionInitializer.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -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()); diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs index aa03c46b6e..3449e937c6 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorNodeWriter.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -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) diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs index 547d763cc0..b3dea7c910 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs @@ -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) diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentRewritePass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentRewritePass.cs index fd40cf241f..0ecaff3b92 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentRewritePass.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentRewritePass.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor { // Per the HTML spec, the following elements are inherently self-closing // For example, is the same as (and therefore it cannot contain descendants) - private readonly static HashSet VoidElements = new HashSet(StringComparer.OrdinalIgnoreCase) + public readonly static HashSet VoidElements = new HashSet(StringComparer.OrdinalIgnoreCase) { "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr", }; diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/HtmlBlockIntermediateNode.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/HtmlBlockIntermediateNode.cs new file mode 100644 index 0000000000..ea9df100cb --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/HtmlBlockIntermediateNode.cs @@ -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(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; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/HtmlBlockPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/HtmlBlockPass.cs new file mode 100644 index 0000000000..b6371a39e2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/HtmlBlockPass.cs @@ -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: + //
+ // click me + //
+ // + // We would store both the div and a tag in a list, but make sure to visit + // the div first. Then when we process the div (recursively), we would remove + // the a from the list. + private class FindHtmlTreeVisitor : + IntermediateNodeWalker, + IExtensionIntermediateNodeVisitor + { + private bool _foundNonHtml; + + public List Trees { get; } = new List(); + + public override void VisitDefault(IntermediateNode node) + { + // If we get here, we found a non-HTML node. Keep traversing. + _foundNonHtml = true; + base.VisitDefault(node); + } + + public void VisitExtension(HtmlElementIntermediateNode node) + { + // We need to restore the state after processing this node. + // We might have found a leaf-block of HTML, but that shouldn't + // affect our parent's state. + var originalState = _foundNonHtml; + + _foundNonHtml = false; + + if (node.HasDiagnostics) + { + // Treat node with errors as non-HTML - don't let the parent rewrite this either. + _foundNonHtml = true; + } + + if (string.Equals("script", node.TagName, StringComparison.OrdinalIgnoreCase)) + { + // Treat script tags as non-HTML - we trigger errors for script tags + // later. + _foundNonHtml = true; + } + + base.VisitDefault(node); + + if (!_foundNonHtml) + { + Trees.Add(new IntermediateNodeReference(Parent, node)); + } + + _foundNonHtml = originalState |= _foundNonHtml; + } + + public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node) + { + if (node.HasDiagnostics) + { + // Treat node with errors as non-HTML + _foundNonHtml = true; + } + + // Visit Children + base.VisitDefault(node); + } + + public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node) + { + if (node.HasDiagnostics) + { + // Treat node with errors as non-HTML + _foundNonHtml = true; + } + + // Visit Children + base.VisitDefault(node); + } + + public override void VisitHtml(HtmlContentIntermediateNode node) + { + if (node.HasDiagnostics) + { + // Treat node with errors as non-HTML + _foundNonHtml = true; + } + + // Visit Children + base.VisitDefault(node); + } + + public override void VisitToken(IntermediateToken node) + { + if (node.HasDiagnostics) + { + // Treat node with errors as non-HTML + _foundNonHtml = true; + } + + if (node.IsCSharp) + { + _foundNonHtml = true; + } + } + } + + private class RewriteVisitor : + IntermediateNodeWalker, + IExtensionIntermediateNodeVisitor + { + private readonly List _trees; + + public RewriteVisitor(List trees) + { + _trees = trees; + } + + public StringBuilder Builder { get; } = new StringBuilder(); + + public void VisitExtension(HtmlElementIntermediateNode node) + { + for (var i = 0; i < _trees.Count; i++) + { + // Remove this node if it's in the list. This ensures that we don't + // do redundant operations. + if (ReferenceEquals(_trees[i].Node, node)) + { + _trees.RemoveAt(i); + break; + } + } + + var isVoid = ComponentDocumentRewritePass.VoidElements.Contains(node.TagName); + var hasBodyContent = node.Body.Any(); + + Builder.Append("<"); + Builder.Append(node.TagName); + + foreach (var attribute in node.Attributes) + { + Visit(attribute); + } + + // If for some reason a void element contains body, then treat it as a + // start/end tag. Treat non-void elements without body content as self-closing. + if (!hasBodyContent && isVoid) + { + // void + Builder.Append(">"); + return; + } + else if (!hasBodyContent) + { + // self-closing + Builder.Append("/>"); + return; + } + + // start/end tag with body. + Builder.Append(">"); + + foreach (var item in node.Body) + { + Visit(item); + } + + Builder.Append(""); + } + + public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node) + { + Builder.Append(" "); + Builder.Append(node.AttributeName); + + if (node.Children.Count == 0) + { + // Minimized attribute + return; + } + + Builder.Append("=\""); + + // Visit Children + base.VisitDefault(node); + + Builder.Append("\""); + } + + public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node) + { + // Visit Children + base.VisitDefault(node); + } + + public override void VisitHtml(HtmlContentIntermediateNode node) + { + // Visit Children + base.VisitDefault(node); + } + + public override void VisitToken(IntermediateToken node) + { + Builder.Append(node.Content); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RenderBatchWriter.cs b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RenderBatchWriter.cs index 6538e81d26..9e5d36e178 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RenderBatchWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/RenderBatchWriter.cs @@ -183,6 +183,10 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits WriteString(frame.TextContent); WritePadding(_binaryWriter, 8); break; + case RenderTreeFrameType.Markup: + WriteString(frame.MarkupContent); + WritePadding(_binaryWriter, 8); + break; default: throw new ArgumentException($"Unsupported frame type: {frame.FrameType}"); } diff --git a/src/Microsoft.AspNetCore.Blazor/MarkupString.cs b/src/Microsoft.AspNetCore.Blazor/MarkupString.cs new file mode 100644 index 0000000000..c956224647 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor/MarkupString.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Blazor +{ + /// + /// A string value that can be rendered as markup such as HTML. + /// + public struct MarkupString + { + /// + /// Constructs an instance of . + /// + /// The value for the new instance. + public MarkupString(string value) + { + Value = value; + } + + /// + /// Gets the value of the . + /// + public string Value { get; } + + /// + /// Casts a to a . + /// + /// The value. + public static explicit operator MarkupString(string value) + => new MarkupString(value); + + /// + public override string ToString() + => Value ?? string.Empty; + } +} diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs index 23426dc10d..89dad2fd3a 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -67,6 +67,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree entry = entry.WithElementSubtreeLength(_entries.Count - indexOfEntryBeingClosed); } + /// + /// Appends a frame representing markup content. + /// + /// An integer that represents the position of the instruction in the source code. + /// Content for the new markup frame. + public void AddMarkupContent(int sequence, string markupContent) + => Append(RenderTreeFrame.Markup(sequence, markupContent ?? string.Empty)); + /// /// Appends a frame representing text content. /// @@ -94,6 +102,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree } } + /// + /// Appends a frame representing markup content. + /// + /// An integer that represents the position of the instruction in the source code. + /// Content for the new markup frame. + public void AddContent(int sequence, MarkupString markupContent) + => AddMarkupContent(sequence, markupContent.Value); + /// /// Appends a frame representing text content. /// diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs index 3ef39edb34..917ec59b41 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -335,6 +335,19 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree break; } + case RenderTreeFrameType.Markup: + { + var oldMarkup = oldFrame.MarkupContent; + var newMarkup = newFrame.MarkupContent; + if (!string.Equals(oldMarkup, newMarkup, StringComparison.Ordinal)) + { + var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame); + diffContext.Edits.Append(RenderTreeEdit.UpdateMarkup(diffContext.SiblingIndex, referenceFrameIndex)); + } + diffContext.SiblingIndex++; + break; + } + case RenderTreeFrameType.Element: { var oldElementName = oldFrame.ElementName; @@ -494,6 +507,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree break; } case RenderTreeFrameType.Text: + case RenderTreeFrameType.Markup: { var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame); diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex)); @@ -510,6 +524,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree InitializeNewComponentReferenceCaptureFrame(ref diffContext, ref newFrame); break; } + default: + throw new NotImplementedException($"Unexpected frame type during {nameof(InsertNewFrame)}: {newFrame.FrameType}"); } } @@ -548,10 +564,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree break; } case RenderTreeFrameType.Text: + case RenderTreeFrameType.Markup: { diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex)); break; } + default: + throw new NotImplementedException($"Unexpected frame type during {nameof(RemoveOldFrame)}: {oldFrame.FrameType}"); } } diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEdit.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEdit.cs index ffefbd510a..9ea6ded635 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEdit.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEdit.cs @@ -1,6 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; + namespace Microsoft.AspNetCore.Blazor.RenderTree { /// @@ -65,6 +67,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree internal static RenderTreeEdit UpdateText(int siblingIndex, int referenceFrameIndex) => new RenderTreeEdit(RenderTreeEditType.UpdateText, siblingIndex, referenceFrameIndex); + internal static RenderTreeEdit UpdateMarkup(int siblingIndex, int referenceFrameIndex) + => new RenderTreeEdit(RenderTreeEditType.UpdateMarkup, siblingIndex, referenceFrameIndex); + internal static RenderTreeEdit SetAttribute(int siblingIndex, int referenceFrameIndex) => new RenderTreeEdit(RenderTreeEditType.SetAttribute, siblingIndex, referenceFrameIndex); diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEditType.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEditType.cs index 7fcec8b7ca..7a28a78bd4 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEditType.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeEditType.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace Microsoft.AspNetCore.Blazor.RenderTree @@ -45,5 +45,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree /// edit position should move back to the parent frame. /// StepOut = 7, + + /// + /// Indicates that the markup content of the specified frame (which must be a markup frame) + /// should be updated. + /// + UpdateMarkup = 8, } } diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs index 9fc1ea870e..3b89d16ba5 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -167,6 +167,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree /// [FieldOffset(16)] public readonly Action ComponentReferenceCaptureAction; + // -------------------------------------------------------------------------------- + // RenderTreeFrameType.Markup + // -------------------------------------------------------------------------------- + + /// + /// If the property equals , + /// gets the content of the markup frame. Otherwise, the value is undefined. + /// + [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); diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrameType.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrameType.cs index 0b67887472..397e43b016 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrameType.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrameType.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace Microsoft.AspNetCore.Blazor.RenderTree @@ -45,5 +45,10 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree /// Represents an instruction to capture or update a reference to the parent component. /// ComponentReferenceCapture = 7, + + /// + /// Represents a block of markup content. + /// + Markup = 8, } } diff --git a/src/shared/BlazorApi.cs b/src/shared/BlazorApi.cs index d9f07f9993..22b13616e1 100644 --- a/src/shared/BlazorApi.cs +++ b/src/shared/BlazorApi.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace Microsoft.AspNetCore.Blazor.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); diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs index 1fa64aeead..72cb7f574b 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs @@ -339,7 +339,7 @@ namespace Test var component = CompileToComponent(@" @addTagHelper *, TestAssembly -Some textNested text"); +Some textNested text @(""Hello"")"); // 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] diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/Razor/IntermediateNodeWriter.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/Razor/IntermediateNodeWriter.cs index e782358eaa..daf38c1251 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/Razor/IntermediateNodeWriter.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/Razor/IntermediateNodeWriter.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests public class IntermediateNodeWriter : IntermediateNodeVisitor, IExtensionIntermediateNodeVisitor, + IExtensionIntermediateNodeVisitor, IExtensionIntermediateNodeVisitor, IExtensionIntermediateNodeVisitor, IExtensionIntermediateNodeVisitor, @@ -269,6 +270,11 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests WriteContentNode(node, node.TagName); } + void IExtensionIntermediateNodeVisitor.VisitExtension(HtmlBlockIntermediateNode node) + { + WriteContentNode(node, node.Content); + } + void IExtensionIntermediateNodeVisitor.VisitExtension(ComponentExtensionNode node) { WriteContentNode(node, node.TagName, node.TypeName); diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs index ad0c9c90bd..ea3c613510 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs @@ -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("Hello @(\"there\")"); + + // 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("Hello"); // Assert Assert.Collection(GetRenderTree(component), - frame => AssertFrame.Element(frame, "myelem", 2, 0), - frame => AssertFrame.Text(frame, "Hello", 1)); + frame => AssertFrame.Markup(frame, "Hello", 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( + "@(\"Hi\") a b "); + + // 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, "a", 3), + frame => AssertFrame.Text(frame, " ", 4), + frame => AssertFrame.Markup(frame, "b", 5), + frame => AssertFrame.Text(frame, " ", 6)); + } + + [Fact] + public void RendersMarkupStringAsMarkupFrame() + { + // Arrange/Act + var component = CompileToComponent( + "@{ var someMarkup = new MarkupString(\"
Hello
\"); }" + + "

@someMarkup

"); + + // Assert + Assert.Collection(GetRenderTree(component), + frame => AssertFrame.Element(frame, "p", 2, 0), + frame => AssertFrame.Markup(frame, "
Hello
", 1)); + } + + [Fact] + public void SupportsSelfClosingElementsWithDynamicContent() + { + // Arrange/Act + var component = CompileToComponent("Some text so elem isn't at position 0 "); + + // 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 "); @@ -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, "", 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, "", 1)); } [Fact] @@ -126,13 +184,14 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test public void SupportsAttributesWithLiteralValues() { // Arrange/Act - var component = CompileToComponent(""); + var component = CompileToComponent("@(\"Hello\")"); // 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( - ""); - - // 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""; } -"); +"); // 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] diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithChildContent/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithChildContent/TestComponent.codegen.cs index f05179d564..bec2a58f8b 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithChildContent/TestComponent.codegen.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithChildContent/TestComponent.codegen.cs @@ -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, "Nested text"); } )); builder.CloseComponent(); diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithChildContent/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithChildContent/TestComponent.ir.txt index bb2aa71df1..ef778dd366 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithChildContent/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithChildContent/TestComponent.ir.txt @@ -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 - - Nested text 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 diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithElementOnlyChildContent/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithElementOnlyChildContent/TestComponent.codegen.cs index 447f889959..6cad337ff5 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithElementOnlyChildContent/TestComponent.codegen.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithElementOnlyChildContent/TestComponent.codegen.cs @@ -16,12 +16,7 @@ namespace Test { base.BuildRenderTree(builder); builder.OpenComponent(0); - builder.AddAttribute(1, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => { - builder2.OpenElement(2, "child"); - builder2.AddContent(3, "hello"); - builder2.CloseElement(); - } - )); + builder.AddMarkupContent(1, "hello"); builder.CloseComponent(); } #pragma warning restore 1998 diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithElementOnlyChildContent/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithElementOnlyChildContent/TestComponent.ir.txt index 92b0a33433..a232d19fb4 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithElementOnlyChildContent/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/ChildComponent_WithElementOnlyChildContent/TestComponent.ir.txt @@ -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 - - hello diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.codegen.cs index 96e1113792..072760b8c0 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.codegen.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.codegen.cs @@ -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, "further"); + 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; diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.ir.txt index 56e7dd13bf..f35669e7a8 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.ir.txt @@ -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 - - further 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 diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.mappings.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.mappings.txt index 205321b96c..5928e2d3b7 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.mappings.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Component_WithRef_WithChildContent/TestComponent.mappings.txt @@ -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; | diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithCSharpExpression/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithCSharpExpression/TestComponent.codegen.cs index 96cd751df4..a6f8fc8235 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithCSharpExpression/TestComponent.codegen.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithCSharpExpression/TestComponent.codegen.cs @@ -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, "

Hello

"); } #pragma warning restore 1998 } diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt index cd7b1a3b5d..77ccff6f9c 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt @@ -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 - -

Hello

diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithComponent/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithComponent/TestComponent.codegen.cs index 02aa50e7f1..64d828bb4a 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithComponent/TestComponent.codegen.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithComponent/TestComponent.codegen.cs @@ -18,9 +18,7 @@ namespace Test builder.OpenComponent(0); builder.CloseComponent(); builder.AddContent(1, "\n\n"); - builder.OpenElement(2, "h1"); - builder.AddContent(3, "Hello"); - builder.CloseElement(); + builder.AddMarkupContent(2, "

Hello

"); } #pragma warning restore 1998 } diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithComponent/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithComponent/TestComponent.ir.txt index a9a56bf975..a1d8d8b762 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithComponent/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithComponent/TestComponent.ir.txt @@ -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 - -

Hello

diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithDirective/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithDirective/TestComponent.codegen.cs index 2b0c5b1f1b..3d1bc7f18b 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithDirective/TestComponent.codegen.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithDirective/TestComponent.codegen.cs @@ -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, "

Hello

"); } #pragma warning restore 1998 } diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithDirective/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithDirective/TestComponent.ir.txt index ab555f741d..79e8e15da1 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithDirective/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/LeadingWhiteSpace_WithDirective/TestComponent.ir.txt @@ -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 - -

Hello

diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_772/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_772/TestComponent.codegen.cs index e84940c3b4..7d5094f269 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_772/TestComponent.codegen.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_772/TestComponent.codegen.cs @@ -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(3); - builder.AddAttribute(4, "Title", ""); + builder.AddMarkupContent(0, "

Hello, world!

"); + builder.AddContent(1, "\n\nWelcome to your new app.\n\n"); + builder.OpenComponent(2); + builder.AddAttribute(3, "Title", ""); builder.CloseComponent(); } #pragma warning restore 1998 diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_772/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_772/TestComponent.ir.txt index 254965721c..238d257383 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_772/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_772/TestComponent.ir.txt @@ -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 - -

Hello, world!

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 diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_773/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_773/TestComponent.codegen.cs index 6a9b4db856..336eb6e0ec 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_773/TestComponent.codegen.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_773/TestComponent.codegen.cs @@ -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(3); - builder.AddAttribute(4, "Title", "
Test!
"); + builder.AddMarkupContent(0, "

Hello, world!

"); + builder.AddContent(1, "\n\nWelcome to your new app.\n\n"); + builder.OpenComponent(2); + builder.AddAttribute(3, "Title", "
Test!
"); builder.CloseComponent(); } #pragma warning restore 1998 diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_773/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_773/TestComponent.ir.txt index 3cf7b01158..161232aae6 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_773/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/Regression_773/TestComponent.ir.txt @@ -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 - -

Hello, world!

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 diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.codegen.cs index b9e370ab5b..c561840bfd 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.codegen.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.codegen.cs @@ -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, "

Hello

"); + builder.AddContent(1, "\n\n"); + builder.AddContent(2, "My value"); } #pragma warning restore 1998 } diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt index f2dad13635..580bbd8848 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithCSharpExpression/TestComponent.ir.txt @@ -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 - -

Hello

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) diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.codegen.cs index ec5891600f..7cea719d6a 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.codegen.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.codegen.cs @@ -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(3); + builder.AddMarkupContent(0, "

Hello

"); + builder.AddContent(1, "\n\n"); + builder.OpenComponent(2); builder.CloseComponent(); } #pragma warning restore 1998 diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt index db06a72441..ccd7057150 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithComponent/TestComponent.ir.txt @@ -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 - -

Hello

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 diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.codegen.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.codegen.cs index a85626313f..4f1afd7461 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.codegen.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.codegen.cs @@ -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, "

Hello

"); } #pragma warning restore 1998 } diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt index 1435b21d2b..912c6e6252 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/TestFiles/RuntimeCodeGenerationTest/TrailingWhiteSpace_WithDirective/TestComponent.ir.txt @@ -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 - -

Hello

diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs index 845f83ce5a..6ec4aea5b6 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs @@ -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(); + + // 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 diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/HtmlBlockPassTest.cs b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/HtmlBlockPassTest.cs new file mode 100644 index 0000000000..e8f36a824a --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/HtmlBlockPassTest.cs @@ -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().Single()); + }).Engine; + + Pass.Engine = Engine; + } + + private RazorEngine Engine { get; } + + private HtmlBlockPass Pass { get; } + + [Fact] + public void Execute_RewritesHtml_Basic() + { + // Arrange + var document = CreateDocument(@" + + + Hello, World! + +"); + + var expected = NormalizeContent(@" + + + Hello, World! + +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var block = documentNode.FindDescendantNodes().Single(); + Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Execute_RewritesHtml_CSharpInAttributes() + { + // Arrange + var document = CreateDocument(@" + + +
foo
+ +"); + + var expected = NormalizeContent(@"
foo
"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var block = documentNode.FindDescendantNodes().Single(); + Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Execute_RewritesHtml_CSharpInBody() + { + // Arrange + var document = CreateDocument(@" + + +
@foo
+
rewriteme
+
@bar
+ +"); + + var expected = NormalizeContent(@"
rewriteme
"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var block = documentNode.FindDescendantNodes().Single(); + Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Execute_RewritesHtml_SelfClosing() + { + // Arrange + var document = CreateDocument(@""); + + var expected = NormalizeContent(@""); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var block = documentNode.FindDescendantNodes().Single(); + Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Execute_RewritesHtml_Void() + { + // Arrange + var document = CreateDocument(@""); + + var expected = NormalizeContent(@""); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var block = documentNode.FindDescendantNodes().Single(); + Assert.Equal(expected, block.Content, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Execute_CannotRewriteHtml_CSharpInCode() + { + // Arrange + var document = CreateDocument(@" + + @if (some_bool) + { + + @hello + + } +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + Assert.Empty(documentNode.FindDescendantNodes()); + } + + [Fact] + public void Execute_CannotRewriteHtml_Script() + { + // Arrange + var document = CreateDocument(@" + + @if (some_bool) + { + + + + } +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + Assert.Empty(documentNode.FindDescendantNodes()); + } + + // 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(@" + + +"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + Assert.Empty(documentNode.FindDescendantNodes()); + } + + [Fact] + public void Execute_RewritesHtml_MismatchedClosingTag() + { + // Arrange + var document = CreateDocument(@" + +
+
rewriteme
+ +"); + + var expected = NormalizeContent(@"
rewriteme
"); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var block = documentNode.FindDescendantNodes().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().Single().Execute(codeDocument, document); + return document; + } + + private class StaticTagHelperFeature : ITagHelperFeature + { + public RazorEngine Engine { get; set; } + + public List TagHelpers { get; set; } + + public IReadOnlyList GetDescriptors() + { + return TagHelpers; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs index d7b362c175..72ab2608bd 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs @@ -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() { diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs index 169299b55f..7d2bb2923b 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs @@ -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() { diff --git a/test/Microsoft.AspnetCore.Blazor.Server.Test/Circuits/RenderBatchWriterTest.cs b/test/Microsoft.AspnetCore.Blazor.Server.Test/Circuits/RenderBatchWriterTest.cs index 277e48a3b2..636b67b311 100644 --- a/test/Microsoft.AspnetCore.Blazor.Server.Test/Circuits/RenderBatchWriterTest.cs +++ b/test/Microsoft.AspnetCore.Blazor.Server.Test/Circuits/RenderBatchWriterTest.cs @@ -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(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 ); } diff --git a/test/shared/AssertFrame.cs b/test/shared/AssertFrame.cs index 5ff9ac0436..afae68ce5e 100644 --- a/test/shared/AssertFrame.cs +++ b/test/shared/AssertFrame.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -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); diff --git a/test/testapps/BasicTestApp/Index.cshtml b/test/testapps/BasicTestApp/Index.cshtml index bfa961e665..ef1f56bddb 100644 --- a/test/testapps/BasicTestApp/Index.cshtml +++ b/test/testapps/BasicTestApp/Index.cshtml @@ -18,6 +18,7 @@ + diff --git a/test/testapps/BasicTestApp/MarkupBlockComponent.cshtml b/test/testapps/BasicTestApp/MarkupBlockComponent.cshtml new file mode 100644 index 0000000000..421c403593 --- /dev/null +++ b/test/testapps/BasicTestApp/MarkupBlockComponent.cshtml @@ -0,0 +1,44 @@ +@using Microsoft.AspNetCore.Blazor.RenderTree +

Markup blocks

+ +

+ This component contains blocks of static HTML markup that will be + represented in the render instructions as single frames. + + This includes nested elements with attributes. +

+ +

Dynamic markup

+ +

It's also possible to emit markup blocks from render fragments:

+ +
+ [@((RenderFragment)EmitMarkupBlock)] +
+ + + +

Markup string

+ +

It's also possible to declare a value of a special type that renders as markup:

+ +@((MarkupString)myMarkup) + +@functions { + bool changeOutput; + + string myMarkup = "

This is a markup string.

"; + + void EmitMarkupBlock(RenderTreeBuilder builder) + { + // To show we detect and apply changes to markup blocks + if (!changeOutput) + { + builder.AddMarkupContent(0, "Here is an example. We support multiple-top-level nodes."); + } + else + { + builder.AddMarkupContent(0, "The output was changed completely."); + } + } +}