ant-design-blazor/components/core/Internal/HtmlRender.cs
2020-05-29 00:33:49 +08:00

296 lines
12 KiB
C#

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 AntDesign
{
public class HtmlRenderer : Renderer
{
private static readonly HashSet<string> _selfClosingElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
};
private static readonly Task _canceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true));
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).
return _canceledRenderTask;
}
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);
_ = RenderFrames(context, childFrames, 0, childFrames.Count);
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
{
if (_selfClosingElements.Contains(frame.ElementName))
{
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; }
}
}
}