|
|
@@ -3,14 +3,11 @@
|
|
|
|
|
|
using System.Diagnostics;
|
|
|
using System.Text.Encodings.Web;
|
|
|
-using Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure;
|
|
|
using Microsoft.AspNetCore.Components.RenderTree;
|
|
|
|
|
|
-namespace Microsoft.AspNetCore.Components.Web.HtmlRendering;
|
|
|
+namespace Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure;
|
|
|
|
|
|
-// This is OK to be a struct because it never gets passed around anywhere. Other code can't even get an instance
|
|
|
-// of it. It just keeps track of some contextual information during a single synchronous HTML output operation.
|
|
|
-internal ref struct HtmlComponentWriter
|
|
|
+public partial class StaticHtmlRenderer
|
|
|
{
|
|
|
private static readonly HashSet<string> SelfClosingElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
|
{
|
|
|
@@ -18,33 +15,30 @@ internal ref struct HtmlComponentWriter
|
|
|
};
|
|
|
|
|
|
private static readonly HtmlEncoder _htmlEncoder = HtmlEncoder.Default;
|
|
|
- private readonly StaticHtmlRenderer _renderer;
|
|
|
- private readonly TextWriter _output;
|
|
|
private string? _closestSelectValueAsString;
|
|
|
|
|
|
- public static void Write(StaticHtmlRenderer renderer, int componentId, TextWriter output)
|
|
|
+ /// <summary>
|
|
|
+ /// Renders the specified component as HTML to the output.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="componentId">The ID of the component whose current HTML state is to be rendered.</param>
|
|
|
+ /// <param name="output">The output destination.</param>
|
|
|
+ protected internal virtual void WriteComponentHtml(int componentId, TextWriter output)
|
|
|
{
|
|
|
// We're about to walk over some buffers inside the renderer that can be mutated during rendering.
|
|
|
// So, we require exclusive access to the renderer during this synchronous process.
|
|
|
- renderer.Dispatcher.AssertAccess();
|
|
|
+ Dispatcher.AssertAccess();
|
|
|
|
|
|
- var context = new HtmlComponentWriter(renderer, output);
|
|
|
- context.RenderComponent(componentId);
|
|
|
+ var frames = GetCurrentRenderTreeFrames(componentId);
|
|
|
+ RenderFrames(output, frames, 0, frames.Count);
|
|
|
}
|
|
|
|
|
|
- private HtmlComponentWriter(StaticHtmlRenderer renderer, TextWriter output)
|
|
|
- {
|
|
|
- _renderer = renderer;
|
|
|
- _output = output;
|
|
|
- }
|
|
|
-
|
|
|
- private int RenderFrames(ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
|
|
|
+ private int RenderFrames(TextWriter output, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
|
|
|
{
|
|
|
var nextPosition = position;
|
|
|
var endPosition = position + maxElements;
|
|
|
while (position < endPosition)
|
|
|
{
|
|
|
- nextPosition = RenderCore(frames, position);
|
|
|
+ nextPosition = RenderCore(output, frames, position);
|
|
|
if (position == nextPosition)
|
|
|
{
|
|
|
throw new InvalidOperationException("We didn't consume any input.");
|
|
|
@@ -56,6 +50,7 @@ internal ref struct HtmlComponentWriter
|
|
|
}
|
|
|
|
|
|
private int RenderCore(
|
|
|
+ TextWriter output,
|
|
|
ArrayRange<RenderTreeFrame> frames,
|
|
|
int position)
|
|
|
{
|
|
|
@@ -63,19 +58,19 @@ internal ref struct HtmlComponentWriter
|
|
|
switch (frame.FrameType)
|
|
|
{
|
|
|
case RenderTreeFrameType.Element:
|
|
|
- return RenderElement(frames, position);
|
|
|
+ return RenderElement(output, frames, position);
|
|
|
case RenderTreeFrameType.Attribute:
|
|
|
throw new InvalidOperationException($"Attributes should only be encountered within {nameof(RenderElement)}");
|
|
|
case RenderTreeFrameType.Text:
|
|
|
- _htmlEncoder.Encode(_output, frame.TextContent);
|
|
|
+ _htmlEncoder.Encode(output, frame.TextContent);
|
|
|
return ++position;
|
|
|
case RenderTreeFrameType.Markup:
|
|
|
- _output.Write(frame.MarkupContent);
|
|
|
+ output.Write(frame.MarkupContent);
|
|
|
return ++position;
|
|
|
case RenderTreeFrameType.Component:
|
|
|
- return RenderChildComponent(frames, position);
|
|
|
+ return RenderChildComponent(output, frames, position);
|
|
|
case RenderTreeFrameType.Region:
|
|
|
- return RenderFrames(frames, position + 1, frame.RegionSubtreeLength - 1);
|
|
|
+ return RenderFrames(output, frames, position + 1, frame.RegionSubtreeLength - 1);
|
|
|
case RenderTreeFrameType.ElementReferenceCapture:
|
|
|
case RenderTreeFrameType.ComponentReferenceCapture:
|
|
|
return ++position;
|
|
|
@@ -84,15 +79,15 @@ internal ref struct HtmlComponentWriter
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private int RenderElement(ArrayRange<RenderTreeFrame> frames, int position)
|
|
|
+ private int RenderElement(TextWriter output, ArrayRange<RenderTreeFrame> frames, int position)
|
|
|
{
|
|
|
ref var frame = ref frames.Array[position];
|
|
|
- _output.Write('<');
|
|
|
- _output.Write(frame.ElementName);
|
|
|
+ output.Write('<');
|
|
|
+ output.Write(frame.ElementName);
|
|
|
int afterElement;
|
|
|
var isTextArea = string.Equals(frame.ElementName, "textarea", StringComparison.OrdinalIgnoreCase);
|
|
|
// We don't want to include value attribute of textarea element.
|
|
|
- var afterAttributes = RenderAttributes(frames, position + 1, frame.ElementSubtreeLength - 1, !isTextArea, out var capturedValueAttribute);
|
|
|
+ var afterAttributes = RenderAttributes(output, frames, position + 1, frame.ElementSubtreeLength - 1, !isTextArea, 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
|
|
|
@@ -101,13 +96,13 @@ internal ref struct HtmlComponentWriter
|
|
|
&& string.Equals(frame.ElementName, "option", StringComparison.OrdinalIgnoreCase)
|
|
|
&& string.Equals(capturedValueAttribute, _closestSelectValueAsString, StringComparison.Ordinal))
|
|
|
{
|
|
|
- _output.Write(" selected");
|
|
|
+ output.Write(" selected");
|
|
|
}
|
|
|
|
|
|
var remainingElements = frame.ElementSubtreeLength + position - afterAttributes;
|
|
|
if (remainingElements > 0 || isTextArea)
|
|
|
{
|
|
|
- _output.Write('>');
|
|
|
+ output.Write('>');
|
|
|
|
|
|
var isSelect = string.Equals(frame.ElementName, "select", StringComparison.OrdinalIgnoreCase);
|
|
|
if (isSelect)
|
|
|
@@ -119,12 +114,12 @@ internal ref struct HtmlComponentWriter
|
|
|
{
|
|
|
// Textarea is a special type of form field where the value is given as text content instead of a 'value' attribute
|
|
|
// So, if we captured a value attribute, use that instead of any child content
|
|
|
- _htmlEncoder.Encode(_output, capturedValueAttribute);
|
|
|
+ _htmlEncoder.Encode(output, capturedValueAttribute);
|
|
|
afterElement = position + frame.ElementSubtreeLength; // Skip descendants
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- afterElement = RenderChildren(frames, afterAttributes, remainingElements);
|
|
|
+ afterElement = RenderChildren(output, frames, afterAttributes, remainingElements);
|
|
|
}
|
|
|
|
|
|
if (isSelect)
|
|
|
@@ -134,9 +129,9 @@ internal ref struct HtmlComponentWriter
|
|
|
_closestSelectValueAsString = null;
|
|
|
}
|
|
|
|
|
|
- _output.Write("</");
|
|
|
- _output.Write(frame.ElementName);
|
|
|
- _output.Write('>');
|
|
|
+ output.Write("</");
|
|
|
+ output.Write(frame.ElementName);
|
|
|
+ output.Write('>');
|
|
|
Debug.Assert(afterElement == position + frame.ElementSubtreeLength);
|
|
|
return afterElement;
|
|
|
}
|
|
|
@@ -144,21 +139,21 @@ internal ref struct HtmlComponentWriter
|
|
|
{
|
|
|
if (SelfClosingElements.Contains(frame.ElementName))
|
|
|
{
|
|
|
- _output.Write(" />");
|
|
|
+ output.Write(" />");
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- _output.Write("></");
|
|
|
- _output.Write(frame.ElementName);
|
|
|
- _output.Write('>');
|
|
|
+ output.Write("></");
|
|
|
+ output.Write(frame.ElementName);
|
|
|
+ output.Write('>');
|
|
|
}
|
|
|
Debug.Assert(afterAttributes == position + frame.ElementSubtreeLength);
|
|
|
return afterAttributes;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private int RenderAttributes(
|
|
|
- ArrayRange<RenderTreeFrame> frames, int position, int maxElements, bool includeValueAttribute, out string? capturedValueAttribute)
|
|
|
+ private static int RenderAttributes(
|
|
|
+ TextWriter output, ArrayRange<RenderTreeFrame> frames, int position, int maxElements, bool includeValueAttribute, out string? capturedValueAttribute)
|
|
|
{
|
|
|
capturedValueAttribute = null;
|
|
|
|
|
|
@@ -195,16 +190,16 @@ internal ref struct HtmlComponentWriter
|
|
|
switch (frame.AttributeValue)
|
|
|
{
|
|
|
case bool flag when flag:
|
|
|
- _output.Write(' ');
|
|
|
- _output.Write(frame.AttributeName);
|
|
|
+ output.Write(' ');
|
|
|
+ output.Write(frame.AttributeName);
|
|
|
break;
|
|
|
case string value:
|
|
|
- _output.Write(' ');
|
|
|
- _output.Write(frame.AttributeName);
|
|
|
- _output.Write('=');
|
|
|
- _output.Write('\"');
|
|
|
- _htmlEncoder.Encode(_output, value);
|
|
|
- _output.Write('\"');
|
|
|
+ output.Write(' ');
|
|
|
+ output.Write(frame.AttributeName);
|
|
|
+ output.Write('=');
|
|
|
+ output.Write('\"');
|
|
|
+ _htmlEncoder.Encode(output, value);
|
|
|
+ output.Write('\"');
|
|
|
break;
|
|
|
default:
|
|
|
break;
|
|
|
@@ -214,27 +209,21 @@ internal ref struct HtmlComponentWriter
|
|
|
return position + maxElements;
|
|
|
}
|
|
|
|
|
|
- private int RenderChildren(ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
|
|
|
+ private int RenderChildren(TextWriter output, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
|
|
|
{
|
|
|
if (maxElements == 0)
|
|
|
{
|
|
|
return position;
|
|
|
}
|
|
|
|
|
|
- return RenderFrames(frames, position, maxElements);
|
|
|
- }
|
|
|
-
|
|
|
- private void RenderComponent(int componentId)
|
|
|
- {
|
|
|
- var frames = _renderer.GetCurrentRenderTreeFrames(componentId);
|
|
|
- RenderFrames(frames, 0, frames.Count);
|
|
|
+ return RenderFrames(output, frames, position, maxElements);
|
|
|
}
|
|
|
|
|
|
- private int RenderChildComponent(ArrayRange<RenderTreeFrame> frames, int position)
|
|
|
+ private int RenderChildComponent(TextWriter output, ArrayRange<RenderTreeFrame> frames, int position)
|
|
|
{
|
|
|
ref var frame = ref frames.Array[position];
|
|
|
|
|
|
- RenderComponent(frame.ComponentId);
|
|
|
+ WriteComponentHtml(frame.ComponentId, output);
|
|
|
|
|
|
return position + frame.ComponentSubtreeLength;
|
|
|
}
|