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:
parent
17b55b983a
commit
8f072a0711
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
{
|
||||
// Per the HTML spec, the following elements are inherently self-closing
|
||||
// For example, <img> is the same as <img /> (and therefore it cannot contain descendants)
|
||||
private readonly static HashSet<string> VoidElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
public readonly static HashSet<string> VoidElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -67,6 +67,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
entry = entry.WithElementSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a frame representing markup content.
|
||||
/// </summary>
|
||||
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
|
||||
/// <param name="markupContent">Content for the new markup frame.</param>
|
||||
public void AddMarkupContent(int sequence, string markupContent)
|
||||
=> Append(RenderTreeFrame.Markup(sequence, markupContent ?? string.Empty));
|
||||
|
||||
/// <summary>
|
||||
/// Appends a frame representing text content.
|
||||
/// </summary>
|
||||
|
|
@ -94,6 +102,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a frame representing markup content.
|
||||
/// </summary>
|
||||
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
|
||||
/// <param name="markupContent">Content for the new markup frame.</param>
|
||||
public void AddContent(int sequence, MarkupString markupContent)
|
||||
=> AddMarkupContent(sequence, markupContent.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Appends a frame representing text content.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -65,6 +67,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
internal static RenderTreeEdit UpdateText(int siblingIndex, int referenceFrameIndex)
|
||||
=> new RenderTreeEdit(RenderTreeEditType.UpdateText, siblingIndex, referenceFrameIndex);
|
||||
|
||||
internal static RenderTreeEdit UpdateMarkup(int siblingIndex, int referenceFrameIndex)
|
||||
=> new RenderTreeEdit(RenderTreeEditType.UpdateMarkup, siblingIndex, referenceFrameIndex);
|
||||
|
||||
internal static RenderTreeEdit SetAttribute(int siblingIndex, int referenceFrameIndex)
|
||||
=> new RenderTreeEdit(RenderTreeEditType.SetAttribute, siblingIndex, referenceFrameIndex);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||
|
|
@ -45,5 +45,11 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
/// edit position should move back to the parent frame.
|
||||
/// </summary>
|
||||
StepOut = 7,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the markup content of the specified frame (which must be a markup frame)
|
||||
/// should be updated.
|
||||
/// </summary>
|
||||
UpdateMarkup = 8,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -167,6 +167,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
/// </summary>
|
||||
[FieldOffset(16)] public readonly Action<object> ComponentReferenceCaptureAction;
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// RenderTreeFrameType.Markup
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Markup"/>,
|
||||
/// gets the content of the markup frame. Otherwise, the value is undefined.
|
||||
/// </summary>
|
||||
[FieldOffset(16)] public readonly string MarkupContent;
|
||||
|
||||
private RenderTreeFrame(int sequence, string elementName, int elementSubtreeLength)
|
||||
: this()
|
||||
{
|
||||
|
|
@ -245,12 +255,25 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
ComponentReferenceCaptureParentFrameIndex = parentFrameIndex;
|
||||
}
|
||||
|
||||
// If we need further constructors whose signatures clash with the patterns above,
|
||||
// we can add extra args to this general-purpose one.
|
||||
private RenderTreeFrame(int sequence, RenderTreeFrameType frameType, string markupContent)
|
||||
: this()
|
||||
{
|
||||
FrameType = frameType;
|
||||
Sequence = sequence;
|
||||
MarkupContent = markupContent;
|
||||
}
|
||||
|
||||
internal static RenderTreeFrame Element(int sequence, string elementName)
|
||||
=> new RenderTreeFrame(sequence, elementName: elementName, elementSubtreeLength: 0);
|
||||
|
||||
internal static RenderTreeFrame Text(int sequence, string textContent)
|
||||
=> new RenderTreeFrame(sequence, textContent: textContent);
|
||||
|
||||
internal static RenderTreeFrame Markup(int sequence, string markupContent)
|
||||
=> new RenderTreeFrame(sequence, RenderTreeFrameType.Markup, markupContent);
|
||||
|
||||
internal static RenderTreeFrame Attribute(int sequence, string name, MulticastDelegate value)
|
||||
=> new RenderTreeFrame(sequence, attributeName: name, attributeValue: value);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||
|
|
@ -45,5 +45,10 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
/// Represents an instruction to capture or update a reference to the parent component.
|
||||
/// </summary>
|
||||
ComponentReferenceCapture = 7,
|
||||
|
||||
/// <summary>
|
||||
/// Represents a block of markup content.
|
||||
/// </summary>
|
||||
Markup = 8,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ namespace Test
|
|||
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<MyComponent MyAttr=""abc"">Some text<some-child a='1'>Nested text</some-child></MyComponent>");
|
||||
<MyComponent MyAttr=""abc"">Some text<some-child a='1'>Nested text @(""Hello"")</some-child></MyComponent>");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
|
@ -356,9 +356,10 @@ namespace Test
|
|||
Assert.Collection(
|
||||
childFrames,
|
||||
frame => AssertFrame.Text(frame, "Some text", 3),
|
||||
frame => AssertFrame.Element(frame, "some-child", 3, 4),
|
||||
frame => AssertFrame.Element(frame, "some-child", 4, 4),
|
||||
frame => AssertFrame.Attribute(frame, "a", "1", 5),
|
||||
frame => AssertFrame.Text(frame, "Nested text", 6));
|
||||
frame => AssertFrame.Text(frame, "Nested text ", 6),
|
||||
frame => AssertFrame.Text(frame, "Hello", 7));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
|
|||
public class IntermediateNodeWriter :
|
||||
IntermediateNodeVisitor,
|
||||
IExtensionIntermediateNodeVisitor<HtmlElementIntermediateNode>,
|
||||
IExtensionIntermediateNodeVisitor<HtmlBlockIntermediateNode>,
|
||||
IExtensionIntermediateNodeVisitor<ComponentExtensionNode>,
|
||||
IExtensionIntermediateNodeVisitor<ComponentAttributeExtensionNode>,
|
||||
IExtensionIntermediateNodeVisitor<RouteAttributeExtensionNode>,
|
||||
|
|
@ -269,6 +270,11 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
|
|||
WriteContentNode(node, node.TagName);
|
||||
}
|
||||
|
||||
void IExtensionIntermediateNodeVisitor<HtmlBlockIntermediateNode>.VisitExtension(HtmlBlockIntermediateNode node)
|
||||
{
|
||||
WriteContentNode(node, node.Content);
|
||||
}
|
||||
|
||||
void IExtensionIntermediateNodeVisitor<ComponentExtensionNode>.VisitExtension(ComponentExtensionNode node)
|
||||
{
|
||||
WriteContentNode(node, node.TagName, node.TypeName);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
using Microsoft.AspNetCore.Blazor.Layouts;
|
||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||
using Microsoft.AspNetCore.Blazor.Test.Helpers;
|
||||
using Xunit;
|
||||
|
|
@ -74,19 +70,81 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsElements()
|
||||
public void SupportsElementsWithDynamicContent()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent("<myelem>Hello @(\"there\")</myelem>");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "myelem", 3, 0),
|
||||
frame => AssertFrame.Text(frame, "Hello ", 1),
|
||||
frame => AssertFrame.Text(frame, "there", 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsElementsAsStaticBlock()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent("<myelem>Hello</myelem>");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "myelem", 2, 0),
|
||||
frame => AssertFrame.Text(frame, "Hello", 1));
|
||||
frame => AssertFrame.Markup(frame, "<myelem>Hello</myelem>", 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsSelfClosingElements()
|
||||
public void CreatesSeparateMarkupFrameForEachTopLevelStaticElement()
|
||||
{
|
||||
// The JavaScript-side rendering code does not rely on this behavior. It supports
|
||||
// inserting markup frames with arbitrary markup (e.g., multiple top-level elements
|
||||
// or none). This test exists only as an observation of the current behavior rather
|
||||
// than a promise that we never want to change it.
|
||||
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
"<root>@(\"Hi\") <child1>a</child1> <child2><another>b</another></child2> </root>");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "root", 7, 0),
|
||||
frame => AssertFrame.Text(frame, "Hi", 1),
|
||||
frame => AssertFrame.Text(frame, " ", 2),
|
||||
frame => AssertFrame.Markup(frame, "<child1>a</child1>", 3),
|
||||
frame => AssertFrame.Text(frame, " ", 4),
|
||||
frame => AssertFrame.Markup(frame, "<child2><another>b</another></child2>", 5),
|
||||
frame => AssertFrame.Text(frame, " ", 6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersMarkupStringAsMarkupFrame()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
"@{ var someMarkup = new MarkupString(\"<div>Hello</div>\"); }"
|
||||
+ "<p>@someMarkup</p>");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "p", 2, 0),
|
||||
frame => AssertFrame.Markup(frame, "<div>Hello</div>", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsSelfClosingElementsWithDynamicContent()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent("Some text so elem isn't at position 0 <myelem myattr=@(\"val\") />");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0),
|
||||
frame => AssertFrame.Element(frame, "myelem", 2, 1),
|
||||
frame => AssertFrame.Attribute(frame, "myattr", "val", 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsSelfClosingElementsAsStaticBlock()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent("Some text so elem isn't at position 0 <myelem />");
|
||||
|
|
@ -94,7 +152,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0),
|
||||
frame => AssertFrame.Element(frame, "myelem", 1, 1));
|
||||
frame => AssertFrame.Markup(frame, "<myelem/>", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -106,7 +164,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0),
|
||||
frame => AssertFrame.Element(frame, "img", 1, 1));
|
||||
frame => AssertFrame.Markup(frame, "<img>", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -126,13 +184,14 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
public void SupportsAttributesWithLiteralValues()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent("<elem attrib-one=\"Value 1\" a2='v2' />");
|
||||
var component = CompileToComponent("<elem attrib-one=\"Value 1\" a2='v2'>@(\"Hello\")</elem>");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "elem", 3, 0),
|
||||
frame => AssertFrame.Element(frame, "elem", 4, 0),
|
||||
frame => AssertFrame.Attribute(frame, "attrib-one", "Value 1", 1),
|
||||
frame => AssertFrame.Attribute(frame, "a2", "v2", 2));
|
||||
frame => AssertFrame.Attribute(frame, "a2", "v2", 2),
|
||||
frame => AssertFrame.Text(frame, "Hello", 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -222,33 +281,21 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsDataDashAttributesWithLiteralValues()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
"<elem data-abc=\"Hello\" />");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "elem", 2, 0),
|
||||
frame => AssertFrame.Attribute(frame, "data-abc", "Hello", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsDataDashAttributesWithCSharpExpressionValues()
|
||||
public void SupportsDataDashAttributes()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(@"
|
||||
@{
|
||||
var myValue = ""My string"";
|
||||
var myValue = ""Expression value"";
|
||||
}
|
||||
<elem data-abc=""@myValue"" />");
|
||||
<elem data-abc=""Literal value"" data-def=""@myValue"" />");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "elem", 2, 0),
|
||||
frame => AssertFrame.Attribute(frame, "data-abc", "My string", 1));
|
||||
frame => AssertFrame.Element(frame, "elem", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "data-abc", "Literal value", 1),
|
||||
frame => AssertFrame.Attribute(frame, "data-def", "Expression value", 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -19,10 +19,7 @@ namespace Test
|
|||
builder.AddAttribute(1, "MyAttr", "abc");
|
||||
builder.AddAttribute(2, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
|
||||
builder2.AddContent(3, "Some text");
|
||||
builder2.OpenElement(4, "some-child");
|
||||
builder2.AddAttribute(5, "a", "1");
|
||||
builder2.AddContent(6, "Nested text");
|
||||
builder2.CloseElement();
|
||||
builder2.AddMarkupContent(4, "<some-child a=\"1\">Nested text</some-child>");
|
||||
}
|
||||
));
|
||||
builder.CloseComponent();
|
||||
|
|
|
|||
|
|
@ -13,12 +13,7 @@ Document -
|
|||
ComponentExtensionNode - (31:1,0 [91] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - Test.MyComponent
|
||||
HtmlContent - (57:1,26 [9] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (57:1,26 [9] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Some text
|
||||
HtmlElement - (66:1,35 [42] x:\dir\subdir\Test\TestComponent.cshtml) - some-child
|
||||
HtmlAttribute - - -
|
||||
HtmlAttributeValue - -
|
||||
IntermediateToken - - Html - 1
|
||||
HtmlContent - (84:1,53 [11] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (84:1,53 [11] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Nested text
|
||||
HtmlBlock - - <some-child a="1">Nested text</some-child>
|
||||
ComponentAttributeExtensionNode - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - MyAttr - MyAttr
|
||||
HtmlContent - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (52:1,21 [3] x:\dir\subdir\Test\TestComponent.cshtml) - Html - abc
|
||||
|
|
|
|||
|
|
@ -16,12 +16,7 @@ namespace Test
|
|||
{
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenComponent<Test.MyComponent>(0);
|
||||
builder.AddAttribute(1, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
|
||||
builder2.OpenElement(2, "child");
|
||||
builder2.AddContent(3, "hello");
|
||||
builder2.CloseElement();
|
||||
}
|
||||
));
|
||||
builder.AddMarkupContent(1, "<child>hello</child>");
|
||||
builder.CloseComponent();
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
|
|
|
|||
|
|
@ -11,6 +11,4 @@ Document -
|
|||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
ComponentExtensionNode - (31:1,0 [47] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - Test.MyComponent
|
||||
HtmlElement - (44:1,13 [20] x:\dir\subdir\Test\TestComponent.cshtml) - child
|
||||
HtmlContent - (51:1,20 [5] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (51:1,20 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - hello
|
||||
HtmlBlock - - <child>hello</child>
|
||||
|
|
|
|||
|
|
@ -19,13 +19,11 @@ namespace Test
|
|||
builder.AddAttribute(1, "SomeProp", "val");
|
||||
builder.AddAttribute(2, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
|
||||
builder2.AddContent(3, "\n Some ");
|
||||
builder2.OpenElement(4, "el");
|
||||
builder2.AddContent(5, "further");
|
||||
builder2.CloseElement();
|
||||
builder2.AddContent(6, " content\n");
|
||||
builder2.AddMarkupContent(4, "<el>further</el>");
|
||||
builder2.AddContent(5, " content\n");
|
||||
}
|
||||
));
|
||||
builder.AddComponentReferenceCapture(7, (__value) => {
|
||||
builder.AddComponentReferenceCapture(6, (__value) => {
|
||||
#line 2 "x:\dir\subdir\Test\TestComponent.cshtml"
|
||||
myInstance = (Test.MyComponent)__value;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ Document -
|
|||
ComponentExtensionNode - (31:1,0 [96] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - Test.MyComponent
|
||||
HtmlContent - (76:1,45 [11] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (76:1,45 [11] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n Some
|
||||
HtmlElement - (87:2,9 [16] x:\dir\subdir\Test\TestComponent.cshtml) - el
|
||||
HtmlContent - (91:2,13 [7] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (91:2,13 [7] x:\dir\subdir\Test\TestComponent.cshtml) - Html - further
|
||||
HtmlBlock - - <el>further</el>
|
||||
HtmlContent - (103:2,25 [10] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (103:2,25 [10] x:\dir\subdir\Test\TestComponent.cshtml) - Html - content\n
|
||||
RefExtensionNode - (49:1,18 [10] x:\dir\subdir\Test\TestComponent.cshtml) - myInstance - Test.MyComponent
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@ namespace Test
|
|||
base.BuildRenderTree(builder);
|
||||
builder.AddContent(0, "My value");
|
||||
builder.AddContent(1, "\n\n");
|
||||
builder.OpenElement(2, "h1");
|
||||
builder.AddContent(3, "Hello");
|
||||
builder.CloseElement();
|
||||
builder.AddMarkupContent(2, "<h1>Hello</h1>");
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,4 @@ Document -
|
|||
IntermediateToken - (2:0,2 [10] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - "My value"
|
||||
HtmlContent - (13:0,13 [4] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (13:0,13 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n
|
||||
HtmlElement - (17:2,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
|
||||
HtmlContent - (21:2,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (21:2,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
|
||||
HtmlBlock - - <h1>Hello</h1>
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ namespace Test
|
|||
builder.OpenComponent<Test.SomeOtherComponent>(0);
|
||||
builder.CloseComponent();
|
||||
builder.AddContent(1, "\n\n");
|
||||
builder.OpenElement(2, "h1");
|
||||
builder.AddContent(3, "Hello");
|
||||
builder.CloseElement();
|
||||
builder.AddMarkupContent(2, "<h1>Hello</h1>");
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,4 @@ Document -
|
|||
ComponentExtensionNode - (36:2,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - SomeOtherComponent - Test.SomeOtherComponent
|
||||
HtmlContent - (58:2,22 [4] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (58:2,22 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n
|
||||
HtmlElement - (62:4,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
|
||||
HtmlContent - (66:4,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (66:4,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
|
||||
HtmlBlock - - <h1>Hello</h1>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ namespace Test
|
|||
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenElement(0, "h1");
|
||||
builder.AddContent(1, "Hello");
|
||||
builder.CloseElement();
|
||||
builder.AddMarkupContent(0, "<h1>Hello</h1>");
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,4 @@ Document -
|
|||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlElement - (17:2,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
|
||||
HtmlContent - (21:2,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (21:2,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
|
||||
HtmlBlock - - <h1>Hello</h1>
|
||||
|
|
|
|||
|
|
@ -16,12 +16,10 @@ namespace Test
|
|||
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenElement(0, "h1");
|
||||
builder.AddContent(1, "Hello, world!");
|
||||
builder.CloseElement();
|
||||
builder.AddContent(2, "\n\nWelcome to your new app.\n\n");
|
||||
builder.OpenComponent<Test.SurveyPrompt>(3);
|
||||
builder.AddAttribute(4, "Title", "");
|
||||
builder.AddMarkupContent(0, "<h1>Hello, world!</h1>");
|
||||
builder.AddContent(1, "\n\nWelcome to your new app.\n\n");
|
||||
builder.OpenComponent<Test.SurveyPrompt>(2);
|
||||
builder.AddAttribute(3, "Title", "");
|
||||
builder.CloseComponent();
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ Document -
|
|||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlElement - (44:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - h1
|
||||
HtmlContent - (48:3,4 [13] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (48:3,4 [13] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello, world!
|
||||
HtmlBlock - - <h1>Hello, world!</h1>
|
||||
HtmlContent - (66:3,22 [32] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (66:3,22 [32] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\nWelcome to your new app.\n\n
|
||||
ComponentExtensionNode - (98:7,0 [23] x:\dir\subdir\Test\TestComponent.cshtml) - SurveyPrompt - Test.SurveyPrompt
|
||||
|
|
|
|||
|
|
@ -16,12 +16,10 @@ namespace Test
|
|||
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenElement(0, "h1");
|
||||
builder.AddContent(1, "Hello, world!");
|
||||
builder.CloseElement();
|
||||
builder.AddContent(2, "\n\nWelcome to your new app.\n\n");
|
||||
builder.OpenComponent<Test.SurveyPrompt>(3);
|
||||
builder.AddAttribute(4, "Title", "<div>Test!</div>");
|
||||
builder.AddMarkupContent(0, "<h1>Hello, world!</h1>");
|
||||
builder.AddContent(1, "\n\nWelcome to your new app.\n\n");
|
||||
builder.OpenComponent<Test.SurveyPrompt>(2);
|
||||
builder.AddAttribute(3, "Title", "<div>Test!</div>");
|
||||
builder.CloseComponent();
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ Document -
|
|||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlElement - (44:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - h1
|
||||
HtmlContent - (48:3,4 [13] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (48:3,4 [13] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello, world!
|
||||
HtmlBlock - - <h1>Hello, world!</h1>
|
||||
HtmlContent - (66:3,22 [32] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (66:3,22 [32] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\nWelcome to your new app.\n\n
|
||||
ComponentExtensionNode - (98:7,0 [41] x:\dir\subdir\Test\TestComponent.cshtml) - SurveyPrompt - Test.SurveyPrompt
|
||||
|
|
|
|||
|
|
@ -15,11 +15,9 @@ namespace Test
|
|||
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenElement(0, "h1");
|
||||
builder.AddContent(1, "Hello");
|
||||
builder.CloseElement();
|
||||
builder.AddContent(2, "\n\n");
|
||||
builder.AddContent(3, "My value");
|
||||
builder.AddMarkupContent(0, "<h1>Hello</h1>");
|
||||
builder.AddContent(1, "\n\n");
|
||||
builder.AddContent(2, "My value");
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ Document -
|
|||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlElement - (0:0,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
|
||||
HtmlContent - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
|
||||
HtmlBlock - - <h1>Hello</h1>
|
||||
HtmlContent - (14:0,14 [4] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (14:0,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n
|
||||
CSharpExpression - (20:2,2 [10] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
|
|
|
|||
|
|
@ -15,11 +15,9 @@ namespace Test
|
|||
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenElement(0, "h1");
|
||||
builder.AddContent(1, "Hello");
|
||||
builder.CloseElement();
|
||||
builder.AddContent(2, "\n\n");
|
||||
builder.OpenComponent<Test.SomeOtherComponent>(3);
|
||||
builder.AddMarkupContent(0, "<h1>Hello</h1>");
|
||||
builder.AddContent(1, "\n\n");
|
||||
builder.OpenComponent<Test.SomeOtherComponent>(2);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ Document -
|
|||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlElement - (31:1,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
|
||||
HtmlContent - (35:1,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (35:1,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
|
||||
HtmlBlock - - <h1>Hello</h1>
|
||||
HtmlContent - (45:1,14 [4] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (45:1,14 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n
|
||||
ComponentExtensionNode - (49:3,0 [22] x:\dir\subdir\Test\TestComponent.cshtml) - SomeOtherComponent - Test.SomeOtherComponent
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ namespace Test
|
|||
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenElement(0, "h1");
|
||||
builder.AddContent(1, "Hello");
|
||||
builder.CloseElement();
|
||||
builder.AddMarkupContent(0, "<h1>Hello</h1>");
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,4 @@ Document -
|
|||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlElement - (0:0,0 [14] x:\dir\subdir\Test\TestComponent.cshtml) - h1
|
||||
HtmlContent - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (4:0,4 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
|
||||
HtmlBlock - - <h1>Hello</h1>
|
||||
|
|
|
|||
|
|
@ -406,6 +406,42 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
Assert.Equal("Value set after render", inputElement.GetAttribute("value"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRenderMarkupBlocks()
|
||||
{
|
||||
var appElement = MountTestComponent<MarkupBlockComponent>();
|
||||
|
||||
// Static markup
|
||||
Assert.Equal(
|
||||
"attributes",
|
||||
appElement.FindElement(By.CssSelector("p span#attribute-example")).Text);
|
||||
|
||||
// Dynamic markup (from a custom RenderFragment)
|
||||
Assert.Equal(
|
||||
"[Here is an example. We support multiple-top-level nodes.]",
|
||||
appElement.FindElement(By.Id("dynamic-markup-block")).Text);
|
||||
Assert.Equal(
|
||||
"example",
|
||||
appElement.FindElement(By.CssSelector("#dynamic-markup-block strong#dynamic-element em")).Text);
|
||||
|
||||
// Dynamic markup (from a MarkupString)
|
||||
Assert.Equal(
|
||||
"This is a markup string.",
|
||||
appElement.FindElement(By.ClassName("markup-string-value")).Text);
|
||||
Assert.Equal(
|
||||
"markup string",
|
||||
appElement.FindElement(By.CssSelector(".markup-string-value em")).Text);
|
||||
|
||||
// Updating markup blocks
|
||||
appElement.FindElement(By.TagName("button")).Click();
|
||||
WaitAssert.Equal(
|
||||
"[The output was changed completely.]",
|
||||
() => appElement.FindElement(By.Id("dynamic-markup-block")).Text);
|
||||
Assert.Equal(
|
||||
"changed",
|
||||
appElement.FindElement(By.CssSelector("#dynamic-markup-block span em")).Text);
|
||||
}
|
||||
|
||||
static IAlert SwitchToAlert(IWebDriver driver)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
|
|||
RenderTreeEdit.UpdateText(105, 106),
|
||||
RenderTreeEdit.StepIn(107),
|
||||
RenderTreeEdit.StepOut(),
|
||||
RenderTreeEdit.UpdateMarkup(108, 109),
|
||||
};
|
||||
var bytes = Serialize(new RenderBatch(
|
||||
new ArrayRange<RenderTreeDiff>(new[]
|
||||
|
|
@ -165,14 +166,15 @@ namespace Microsoft.AspNetCore.Blazor.Server
|
|||
|
||||
AssertBinaryContents(bytes, 0,
|
||||
123, // Component ID for diff 0
|
||||
7, // diff[0].Edits.Count
|
||||
8, // diff[0].Edits.Count
|
||||
RenderTreeEditType.PrependFrame, 456, 789, NullStringMarker,
|
||||
RenderTreeEditType.RemoveFrame, 101, 0, NullStringMarker,
|
||||
RenderTreeEditType.SetAttribute, 102, 103, NullStringMarker,
|
||||
RenderTreeEditType.RemoveAttribute, 104, 0, "Some removed attribute",
|
||||
RenderTreeEditType.UpdateText, 105, 106, NullStringMarker,
|
||||
RenderTreeEditType.StepIn, 107, 0, NullStringMarker,
|
||||
RenderTreeEditType.StepOut, 0, 0, NullStringMarker
|
||||
RenderTreeEditType.StepOut, 0, 0, NullStringMarker,
|
||||
RenderTreeEditType.UpdateMarkup, 108, 109, NullStringMarker
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -198,14 +200,15 @@ namespace Microsoft.AspNetCore.Blazor.Server
|
|||
RenderTreeFrame.Region(130)
|
||||
.WithRegionSubtreeLength(1234),
|
||||
RenderTreeFrame.Text(131, "Some text"),
|
||||
}, 9),
|
||||
RenderTreeFrame.Markup(132, "Some markup"),
|
||||
}, 10),
|
||||
default,
|
||||
default));
|
||||
|
||||
// Assert
|
||||
var referenceFramesStartIndex = ReadInt(bytes, bytes.Length - 16);
|
||||
AssertBinaryContents(bytes, referenceFramesStartIndex,
|
||||
9, // Number of frames
|
||||
10, // Number of frames
|
||||
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0,
|
||||
RenderTreeFrameType.Attribute, "Attribute with nonstring value", NullStringMarker, 0,
|
||||
RenderTreeFrameType.Attribute, "Attribute with delegate value", NullStringMarker, 789,
|
||||
|
|
@ -214,7 +217,8 @@ namespace Microsoft.AspNetCore.Blazor.Server
|
|||
RenderTreeFrameType.Element, 1234, "Some element", 0,
|
||||
RenderTreeFrameType.ElementReferenceCapture, "my unique ID", 0, 0,
|
||||
RenderTreeFrameType.Region, 1234, 0, 0,
|
||||
RenderTreeFrameType.Text, "Some text", 0, 0
|
||||
RenderTreeFrameType.Text, "Some text", 0, 0,
|
||||
RenderTreeFrameType.Markup, "Some markup", 0, 0
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<option value="BasicTestApp.RedTextComponent">Red text</option>
|
||||
<option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option>
|
||||
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
|
||||
<option value="BasicTestApp.MarkupBlockComponent">Markup blocks</option>
|
||||
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
|
||||
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
|
||||
<option value="BasicTestApp.HttpClientTest.BinaryHttpRequestsComponent">Binary HttpClient tester</option>
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue