2020-03-11 17:15:15 +08:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
using System.Runtime.ExceptionServices;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Microsoft.AspNetCore.Components;
|
|
|
|
|
using Microsoft.AspNetCore.Components.RenderTree;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
|
|
|
|
namespace AntBlazor
|
|
|
|
|
{
|
|
|
|
|
public class HtmlRenderer : Renderer
|
|
|
|
|
{
|
2020-04-23 17:13:56 +08:00
|
|
|
|
private static readonly HashSet<string> _selfClosingElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
2020-03-11 17:15:15 +08:00
|
|
|
|
{
|
|
|
|
|
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
|
|
|
|
|
};
|
|
|
|
|
|
2020-04-23 17:13:56 +08:00
|
|
|
|
private static readonly Task _canceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true));
|
2020-03-11 17:15:15 +08:00
|
|
|
|
|
|
|
|
|
private readonly Func<string, string> _htmlEncoder;
|
|
|
|
|
|
|
|
|
|
public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, Func<string, string> htmlEncoder)
|
|
|
|
|
: base(serviceProvider, loggerFactory)
|
|
|
|
|
{
|
|
|
|
|
_htmlEncoder = htmlEncoder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
|
|
|
|
|
{
|
|
|
|
|
// By default we return a canceled task. This has the effect of making it so that the
|
|
|
|
|
// OnAfterRenderAsync callbacks on components don't run by default.
|
|
|
|
|
// This way, by default prerendering gets the correct behavior and other renderers
|
|
|
|
|
// override the UpdateDisplayAsync method already, so those components can
|
|
|
|
|
// either complete a task when the client acknowledges the render, or return a canceled task
|
|
|
|
|
// when the renderer gets disposed.
|
|
|
|
|
|
|
|
|
|
// We believe that returning a canceled task is the right behavior as we expect that any class
|
|
|
|
|
// that subclasses this class to provide an implementation for a given rendering scenario respects
|
|
|
|
|
// the contract that OnAfterRender should only be called when the display has successfully been updated
|
|
|
|
|
// and the application is interactive. (Element and component references are populated and JavaScript interop
|
|
|
|
|
// is available).
|
2020-04-23 17:13:56 +08:00
|
|
|
|
return _canceledRenderTask;
|
2020-03-11 17:15:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<ComponentRenderedText> RenderComponentAsync(IComponent component, ParameterView initialParameters)
|
|
|
|
|
{
|
|
|
|
|
var (componentId, frames) = await CreateInitialRenderAsync(component, initialParameters);
|
|
|
|
|
|
|
|
|
|
var context = new HtmlRenderingContext();
|
|
|
|
|
var newPosition = RenderFrames(context, frames, 0, frames.Count);
|
|
|
|
|
Debug.Assert(newPosition == frames.Count);
|
|
|
|
|
return new ComponentRenderedText(componentId, context.Result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<ComponentRenderedText> RenderComponentAsync(Type componentType, ParameterView initialParameters)
|
|
|
|
|
{
|
|
|
|
|
var (componentId, frames) = await CreateInitialRenderAsync(componentType, initialParameters);
|
|
|
|
|
|
|
|
|
|
var context = new HtmlRenderingContext();
|
|
|
|
|
var newPosition = RenderFrames(context, frames, 0, frames.Count);
|
|
|
|
|
Debug.Assert(newPosition == frames.Count);
|
|
|
|
|
return new ComponentRenderedText(componentId, context.Result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<ComponentRenderedText> RenderComponentAsync<TComponent>(ParameterView initialParameters) where TComponent : IComponent
|
|
|
|
|
{
|
|
|
|
|
return RenderComponentAsync(typeof(TComponent), initialParameters);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
protected override void HandleException(Exception exception)
|
|
|
|
|
=> ExceptionDispatchInfo.Capture(exception).Throw();
|
|
|
|
|
|
|
|
|
|
private int RenderFrames(HtmlRenderingContext context, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
|
|
|
|
|
{
|
|
|
|
|
var nextPosition = position;
|
|
|
|
|
var endPosition = position + maxElements;
|
|
|
|
|
while (position < endPosition)
|
|
|
|
|
{
|
|
|
|
|
nextPosition = RenderCore(context, frames, position);
|
|
|
|
|
if (position == nextPosition)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("We didn't consume any input.");
|
|
|
|
|
}
|
|
|
|
|
position = nextPosition;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nextPosition;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int RenderCore(
|
|
|
|
|
HtmlRenderingContext context,
|
|
|
|
|
ArrayRange<RenderTreeFrame> frames,
|
|
|
|
|
int position)
|
|
|
|
|
{
|
|
|
|
|
ref var frame = ref frames.Array[position];
|
|
|
|
|
switch (frame.FrameType)
|
|
|
|
|
{
|
|
|
|
|
case RenderTreeFrameType.Element:
|
|
|
|
|
return RenderElement(context, frames, position);
|
|
|
|
|
|
|
|
|
|
case RenderTreeFrameType.Attribute:
|
|
|
|
|
throw new InvalidOperationException($"Attributes should only be encountered within {nameof(RenderElement)}");
|
|
|
|
|
case RenderTreeFrameType.Text:
|
|
|
|
|
context.Result.Add(_htmlEncoder(frame.TextContent));
|
|
|
|
|
return ++position;
|
|
|
|
|
|
|
|
|
|
case RenderTreeFrameType.Markup:
|
|
|
|
|
context.Result.Add(frame.MarkupContent);
|
|
|
|
|
return ++position;
|
|
|
|
|
|
|
|
|
|
case RenderTreeFrameType.Component:
|
|
|
|
|
return RenderChildComponent(context, frames, position);
|
|
|
|
|
|
|
|
|
|
case RenderTreeFrameType.Region:
|
|
|
|
|
return RenderFrames(context, frames, position + 1, frame.RegionSubtreeLength - 1);
|
|
|
|
|
|
|
|
|
|
case RenderTreeFrameType.ElementReferenceCapture:
|
|
|
|
|
case RenderTreeFrameType.ComponentReferenceCapture:
|
|
|
|
|
return ++position;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
throw new InvalidOperationException($"Invalid element frame type '{frame.FrameType}'.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int RenderChildComponent(
|
|
|
|
|
HtmlRenderingContext context,
|
|
|
|
|
ArrayRange<RenderTreeFrame> frames,
|
|
|
|
|
int position)
|
|
|
|
|
{
|
|
|
|
|
ref var frame = ref frames.Array[position];
|
|
|
|
|
var childFrames = GetCurrentRenderTreeFrames(frame.ComponentId);
|
2020-04-24 18:32:50 +08:00
|
|
|
|
_ = RenderFrames(context, childFrames, 0, childFrames.Count);
|
2020-03-11 17:15:15 +08:00
|
|
|
|
return position + frame.ComponentSubtreeLength;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int RenderElement(
|
|
|
|
|
HtmlRenderingContext context,
|
|
|
|
|
ArrayRange<RenderTreeFrame> frames,
|
|
|
|
|
int position)
|
|
|
|
|
{
|
|
|
|
|
ref var frame = ref frames.Array[position];
|
|
|
|
|
var result = context.Result;
|
|
|
|
|
result.Add("<");
|
|
|
|
|
result.Add(frame.ElementName);
|
|
|
|
|
var afterAttributes = RenderAttributes(context, frames, position + 1, frame.ElementSubtreeLength - 1, out var capturedValueAttribute);
|
|
|
|
|
|
|
|
|
|
// When we see an <option> as a descendant of a <select>, and the option's "value" attribute matches the
|
|
|
|
|
// "value" attribute on the <select>, then we auto-add the "selected" attribute to that option. This is
|
|
|
|
|
// a way of converting Blazor's select binding feature to regular static HTML.
|
|
|
|
|
if (context.ClosestSelectValueAsString != null
|
|
|
|
|
&& string.Equals(frame.ElementName, "option", StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
&& string.Equals(capturedValueAttribute, context.ClosestSelectValueAsString, StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
result.Add(" selected");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var remainingElements = frame.ElementSubtreeLength + position - afterAttributes;
|
|
|
|
|
if (remainingElements > 0)
|
|
|
|
|
{
|
|
|
|
|
result.Add(">");
|
|
|
|
|
|
|
|
|
|
var isSelect = string.Equals(frame.ElementName, "select", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
if (isSelect)
|
|
|
|
|
{
|
|
|
|
|
context.ClosestSelectValueAsString = capturedValueAttribute;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var afterElement = RenderChildren(context, frames, afterAttributes, remainingElements);
|
|
|
|
|
|
|
|
|
|
if (isSelect)
|
|
|
|
|
{
|
|
|
|
|
// There's no concept of nested <select> elements, so as soon as we're exiting one of them,
|
|
|
|
|
// we can safely say there is no longer any value for this
|
|
|
|
|
context.ClosestSelectValueAsString = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.Add("</");
|
|
|
|
|
result.Add(frame.ElementName);
|
|
|
|
|
result.Add(">");
|
|
|
|
|
Debug.Assert(afterElement == position + frame.ElementSubtreeLength);
|
|
|
|
|
return afterElement;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2020-04-23 17:13:56 +08:00
|
|
|
|
if (_selfClosingElements.Contains(frame.ElementName))
|
2020-03-11 17:15:15 +08:00
|
|
|
|
{
|
|
|
|
|
result.Add(" />");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
result.Add(">");
|
|
|
|
|
result.Add("</");
|
|
|
|
|
result.Add(frame.ElementName);
|
|
|
|
|
result.Add(">");
|
|
|
|
|
}
|
|
|
|
|
Debug.Assert(afterAttributes == position + frame.ElementSubtreeLength);
|
|
|
|
|
return afterAttributes;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int RenderChildren(HtmlRenderingContext context, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
|
|
|
|
|
{
|
|
|
|
|
if (maxElements == 0)
|
|
|
|
|
{
|
|
|
|
|
return position;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return RenderFrames(context, frames, position, maxElements);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int RenderAttributes(
|
|
|
|
|
HtmlRenderingContext context,
|
|
|
|
|
ArrayRange<RenderTreeFrame> frames, int position, int maxElements, out string capturedValueAttribute)
|
|
|
|
|
{
|
|
|
|
|
capturedValueAttribute = null;
|
|
|
|
|
|
|
|
|
|
if (maxElements == 0)
|
|
|
|
|
{
|
|
|
|
|
return position;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = context.Result;
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < maxElements; i++)
|
|
|
|
|
{
|
|
|
|
|
var candidateIndex = position + i;
|
|
|
|
|
ref var frame = ref frames.Array[candidateIndex];
|
|
|
|
|
if (frame.FrameType != RenderTreeFrameType.Attribute)
|
|
|
|
|
{
|
|
|
|
|
return candidateIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (frame.AttributeName.Equals("value", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
capturedValueAttribute = frame.AttributeValue as string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (frame.AttributeValue)
|
|
|
|
|
{
|
|
|
|
|
case bool flag when flag:
|
|
|
|
|
result.Add(" ");
|
|
|
|
|
result.Add(frame.AttributeName);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case string value:
|
|
|
|
|
result.Add(" ");
|
|
|
|
|
result.Add(frame.AttributeName);
|
|
|
|
|
result.Add("=");
|
|
|
|
|
result.Add("\"");
|
|
|
|
|
result.Add(_htmlEncoder(value));
|
|
|
|
|
result.Add("\"");
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return position + maxElements;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<(int, ArrayRange<RenderTreeFrame>)> CreateInitialRenderAsync(Type componentType, ParameterView initialParameters)
|
|
|
|
|
{
|
|
|
|
|
var component = InstantiateComponent(componentType);
|
|
|
|
|
var componentId = AssignRootComponentId(component);
|
|
|
|
|
|
|
|
|
|
await RenderRootComponentAsync(componentId, initialParameters);
|
|
|
|
|
|
|
|
|
|
return (componentId, GetCurrentRenderTreeFrames(componentId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<(int, ArrayRange<RenderTreeFrame>)> CreateInitialRenderAsync(IComponent component, ParameterView initialParameters)
|
|
|
|
|
{
|
|
|
|
|
var componentId = AssignRootComponentId(component);
|
|
|
|
|
|
|
|
|
|
await RenderRootComponentAsync(componentId, initialParameters);
|
|
|
|
|
|
|
|
|
|
return (componentId, GetCurrentRenderTreeFrames(componentId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class HtmlRenderingContext
|
|
|
|
|
{
|
|
|
|
|
public List<string> Result { get; } = new List<string>();
|
|
|
|
|
|
|
|
|
|
public string ClosestSelectValueAsString { get; set; }
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-23 17:13:56 +08:00
|
|
|
|
}
|