Skip to content

Commit 436b546

Browse files
authored
[Components] Create a renderer to render components into HTML (#4463)
* Adds an HtmlRenderer to Microsoft.AspNetCore.Components * It renders the component into a list of strings. * It only handles synchronous rendering.
1 parent a7f6154 commit 436b546

File tree

7 files changed

+709
-3
lines changed

7 files changed

+709
-3
lines changed

src/Components/Components.sln

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.27130.2010
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.28315.86
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA}"
77
EndProject

src/Components/build/dependencies.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
<InternalAspNetCoreSdkPackageVersion>3.0.0-alpha1-20181011.3</InternalAspNetCoreSdkPackageVersion>
1010
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.2.0-preview1-34576</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
1111
<MicrosoftAspNetCoreAppPackageVersion>3.0.0-alpha1-10605</MicrosoftAspNetCoreAppPackageVersion>
12+
<MicrosoftAspNetCoreHtmlAbstractionsPackageVersion>3.0.0-alpha1-10605</MicrosoftAspNetCoreHtmlAbstractionsPackageVersion>
1213
<MicrosoftAspNetCoreRazorDesignPackageVersion>3.0.0-alpha1-10605</MicrosoftAspNetCoreRazorDesignPackageVersion>
14+
<MicrosoftExtensionsDependencyInjectionPackageVersion>3.0.0-alpha1-10605</MicrosoftExtensionsDependencyInjectionPackageVersion>
1315
<MicrosoftNETCoreAppPackageVersion>3.0.0-preview1-26907-05</MicrosoftNETCoreAppPackageVersion>
1416
<SignalRPackageVersion>3.0.0-alpha1-10605</SignalRPackageVersion>
1517
<TemplateBlazorPackageVersion>0.8.0-preview1-20181122.3</TemplateBlazorPackageVersion>

src/Components/src/Microsoft.AspNetCore.Components/Rendering/ComponentState.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ internal class ComponentState
2929
public int ComponentId => _componentId;
3030
public IComponent Component => _component;
3131
public ComponentState ParentComponentState => _parentComponentState;
32+
public RenderTreeBuilder CurrrentRenderTree => _renderTreeBuilderCurrent;
3233

3334
/// <summary>
3435
/// Constructs an instance of <see cref="ComponentState"/>.
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Components.RenderTree;
9+
10+
namespace Microsoft.AspNetCore.Components.Rendering
11+
{
12+
/// <summary>
13+
/// A <see cref="Renderer"/> that produces HTML.
14+
/// </summary>
15+
public class HtmlRenderer : Renderer
16+
{
17+
private static readonly HashSet<string> SelfClosingElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
18+
{
19+
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
20+
};
21+
22+
private readonly Func<string, string> _htmlEncoder;
23+
24+
/// <summary>
25+
/// Initializes a new instance of <see cref="HtmlRenderer"/>.
26+
/// </summary>
27+
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use to instantiate components.</param>
28+
/// <param name="htmlEncoder">A <see cref="Func{T, TResult}"/> that will HTML encode the given string.</param>
29+
public HtmlRenderer(IServiceProvider serviceProvider, Func<string, string> htmlEncoder) : base(serviceProvider)
30+
{
31+
_htmlEncoder = htmlEncoder;
32+
}
33+
34+
/// <inheritdoc />
35+
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
36+
{
37+
return Task.CompletedTask;
38+
}
39+
40+
/// <summary>
41+
/// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
42+
/// of the HTML produced by the component.
43+
/// </summary>
44+
/// <typeparam name="T">The type of the <see cref="IComponent"/>.</typeparam>
45+
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
46+
/// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
47+
public IEnumerable<string> RenderComponent<T>(ParameterCollection initialParameters) where T : IComponent
48+
{
49+
return RenderComponent(typeof(T), initialParameters);
50+
}
51+
52+
/// <summary>
53+
/// Renders a component into a sequence of <see cref="string"/> fragments that represent the textual representation
54+
/// of the HTML produced by the component.
55+
/// </summary>
56+
/// <param name="componentType">The type of the <see cref="IComponent"/>.</param>
57+
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
58+
/// <returns>A sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
59+
private IEnumerable<string> RenderComponent(Type componentType, ParameterCollection initialParameters)
60+
{
61+
var frames = CreateInitialRender(componentType, initialParameters);
62+
63+
if (frames.Count == 0)
64+
{
65+
return Array.Empty<string>();
66+
}
67+
else
68+
{
69+
var result = new List<string>();
70+
var newPosition = RenderFrames(result, frames, 0, frames.Count);
71+
Debug.Assert(newPosition == frames.Count);
72+
return result;
73+
}
74+
}
75+
76+
private int RenderFrames(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
77+
{
78+
var nextPosition = position;
79+
var endPosition = position + maxElements;
80+
while (position < endPosition)
81+
{
82+
nextPosition = RenderCore(result, frames, position, maxElements);
83+
if (position == nextPosition)
84+
{
85+
throw new InvalidOperationException("We didn't consume any input.");
86+
}
87+
position = nextPosition;
88+
}
89+
90+
return nextPosition;
91+
}
92+
93+
private int RenderCore(
94+
List<string> result,
95+
ArrayRange<RenderTreeFrame> frames,
96+
int position,
97+
int length)
98+
{
99+
ref var frame = ref frames.Array[position];
100+
switch (frame.FrameType)
101+
{
102+
case RenderTreeFrameType.Element:
103+
return RenderElement(result, frames, position);
104+
case RenderTreeFrameType.Attribute:
105+
return RenderAttributes(result, frames, position, 1);
106+
case RenderTreeFrameType.Text:
107+
result.Add(_htmlEncoder(frame.TextContent));
108+
return ++position;
109+
case RenderTreeFrameType.Markup:
110+
result.Add(frame.MarkupContent);
111+
return ++position;
112+
case RenderTreeFrameType.Component:
113+
return RenderChildComponent(result, frames, position);
114+
case RenderTreeFrameType.Region:
115+
return RenderFrames(result, frames, position + 1, frame.RegionSubtreeLength - 1);
116+
case RenderTreeFrameType.ElementReferenceCapture:
117+
case RenderTreeFrameType.ComponentReferenceCapture:
118+
return ++position;
119+
default:
120+
throw new InvalidOperationException($"Invalid element frame type '{frame.FrameType}'.");
121+
}
122+
}
123+
124+
private int RenderChildComponent(
125+
List<string> result,
126+
ArrayRange<RenderTreeFrame> frames,
127+
int position)
128+
{
129+
ref var frame = ref frames.Array[position];
130+
var childFrames = GetCurrentRenderTreeFrames(frame.ComponentId);
131+
RenderFrames(result, childFrames, 0, childFrames.Count);
132+
return position + frame.ComponentSubtreeLength;
133+
}
134+
135+
private int RenderElement(
136+
List<string> result,
137+
ArrayRange<RenderTreeFrame> frames,
138+
int position)
139+
{
140+
ref var frame = ref frames.Array[position];
141+
result.Add("<");
142+
result.Add(frame.ElementName);
143+
var afterAttributes = RenderAttributes(result, frames, position + 1, frame.ElementSubtreeLength - 1);
144+
var remainingElements = frame.ElementSubtreeLength + position - afterAttributes;
145+
if (remainingElements > 0)
146+
{
147+
result.Add(">");
148+
var afterElement = RenderChildren(result, frames, afterAttributes, remainingElements);
149+
result.Add("</");
150+
result.Add(frame.ElementName);
151+
result.Add(">");
152+
Debug.Assert(afterElement == position + frame.ElementSubtreeLength);
153+
return afterElement;
154+
}
155+
else
156+
{
157+
if (SelfClosingElements.Contains(frame.ElementName))
158+
{
159+
result.Add(" />");
160+
}
161+
else
162+
{
163+
result.Add(">");
164+
result.Add("</");
165+
result.Add(frame.ElementName);
166+
result.Add(">");
167+
}
168+
Debug.Assert(afterAttributes == position + frame.ElementSubtreeLength);
169+
return afterAttributes;
170+
}
171+
}
172+
173+
private int RenderChildren(List<string> result, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
174+
{
175+
if (maxElements == 0)
176+
{
177+
return position;
178+
}
179+
180+
return RenderFrames(result, frames, position, maxElements);
181+
}
182+
183+
private int RenderAttributes(
184+
List<string> result,
185+
ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
186+
{
187+
if (maxElements == 0)
188+
{
189+
return position;
190+
}
191+
192+
for (var i = 0; i < maxElements; i++)
193+
{
194+
var candidateIndex = position + i;
195+
ref var frame = ref frames.Array[candidateIndex];
196+
if (frame.FrameType != RenderTreeFrameType.Attribute)
197+
{
198+
return candidateIndex;
199+
}
200+
201+
switch (frame.AttributeValue)
202+
{
203+
case bool flag when flag:
204+
result.Add(" ");
205+
result.Add(frame.AttributeName);
206+
break;
207+
case string value:
208+
result.Add(" ");
209+
result.Add(frame.AttributeName);
210+
result.Add("=");
211+
result.Add("\"");
212+
result.Add(_htmlEncoder(value));
213+
result.Add("\"");
214+
break;
215+
default:
216+
break;
217+
}
218+
}
219+
220+
return position + maxElements;
221+
}
222+
223+
private ArrayRange<RenderTreeFrame> CreateInitialRender(Type componentType, ParameterCollection initialParameters)
224+
{
225+
var component = InstantiateComponent(componentType);
226+
var componentId = AssignRootComponentId(component);
227+
228+
RenderRootComponent(componentId, initialParameters);
229+
230+
return GetCurrentRenderTreeFrames(componentId);
231+
}
232+
}
233+
}
234+

src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ protected IComponent InstantiateComponent(Type componentType)
5353
protected int AssignRootComponentId(IComponent component)
5454
=> AttachAndInitComponent(component, -1).ComponentId;
5555

56+
/// <summary>
57+
/// Gets the current render tree for a given component.
58+
/// </summary>
59+
/// <param name="componentId">The id for the component.</param>
60+
/// <returns>The <see cref="RenderTreeBuilder"/> representing the current render tree.</returns>
61+
private protected ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId) => GetRequiredComponentState(componentId).CurrrentRenderTree.GetFrames();
62+
5663
/// <summary>
5764
/// Performs the first render for a root component. After this, the root component
5865
/// makes its own decisions about when to re-render, so there is no need to call
@@ -65,6 +72,19 @@ protected void RenderRootComponent(int componentId)
6572
.SetDirectParameters(ParameterCollection.Empty);
6673
}
6774

75+
/// <summary>
76+
/// Performs the first render for a root component. After this, the root component
77+
/// makes its own decisions about when to re-render, so there is no need to call
78+
/// this more than once.
79+
/// </summary>
80+
/// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
81+
/// <param name="initialParameters">The <see cref="ParameterCollection"/>with the initial parameters to use for rendering.</param>
82+
protected void RenderRootComponent(int componentId, ParameterCollection initialParameters)
83+
{
84+
GetRequiredComponentState(componentId)
85+
.SetDirectParameters(initialParameters);
86+
}
87+
6888
private ComponentState AttachAndInitComponent(IComponent component, int parentComponentId)
6989
{
7090
var componentId = _nextComponentId++;

src/Components/test/Microsoft.AspNetCore.Components.Test/Microsoft.AspNetCore.Components.Test.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netcoreapp3.0</TargetFramework>
@@ -11,6 +11,8 @@
1111
<PackageReference Include="xunit" Version="2.3.1" />
1212
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
1313
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
14+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
15+
<PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="$(MicrosoftAspNetCoreHtmlAbstractionsPackageVersion)" />
1416
</ItemGroup>
1517

1618
<ItemGroup>

0 commit comments

Comments
 (0)