Skip to content

Commit

Permalink
Refactor layout
Browse files Browse the repository at this point in the history
  • Loading branch information
ltrzesniewski committed Dec 2, 2023
1 parent 162c1aa commit 3e8cbdc
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 96 deletions.
53 changes: 17 additions & 36 deletions src/RazorBlade.Library/HtmlLayout.cs
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -15,7 +14,7 @@ public abstract class HtmlLayout : HtmlTemplate, IRazorLayout

private IRazorLayout.IExecutionResult LayoutInput => _layoutInput ?? throw new InvalidOperationException("No layout is being rendered.");

async Task<IRazorLayout.IExecutionResult> IRazorLayout.RenderLayoutAsync(IRazorLayout.IExecutionResult input)
async Task<IRazorLayout.IExecutionResult> IRazorLayout.ExecuteLayoutAsync(IRazorLayout.IExecutionResult input)
{
input.CancellationToken.ThrowIfCancellationRequested();
var previousStatus = (Output, CancellationToken);
Expand All @@ -24,20 +23,15 @@ async Task<IRazorLayout.IExecutionResult> IRazorLayout.RenderLayoutAsync(IRazorL
{
_layoutInput = input;

var stringWriter = new StringWriter();
var output = new StringWriter();

Output = stringWriter;
Output = output;
CancellationToken = input.CancellationToken;
// TODO fully reset/restore the state

await ExecuteAsync().ConfigureAwait(false);

return new ExecutionResult
{
Body = new StringBuilderEncodedContent(stringWriter.GetStringBuilder()),
Layout = Layout,
Sections = _sections,
CancellationToken = CancellationToken
};
return new ExecutionResult(this, output.GetStringBuilder());
}
finally
{
Expand All @@ -49,15 +43,15 @@ async Task<IRazorLayout.IExecutionResult> IRazorLayout.RenderLayoutAsync(IRazorL
/// <summary>
/// Returns the inner page body.
/// </summary>
protected IEncodedContent RenderBody()
protected internal IEncodedContent RenderBody()
=> LayoutInput.Body;

/// <summary>
/// Renders a required section and returns the result as encoded content.
/// </summary>
/// <param name="name">The section name.</param>
/// <returns>The content to write to the output.</returns>
protected IEncodedContent RenderSection(string name)
protected internal IEncodedContent RenderSection(string name)
=> RenderSection(name, true);

/// <summary>
Expand All @@ -66,21 +60,21 @@ protected IEncodedContent RenderSection(string name)
/// <param name="name">The section name.</param>
/// <param name="required">Whether the section is required.</param>
/// <returns>The content to write to the output.</returns>
protected IEncodedContent RenderSection(string name, bool required)
protected internal IEncodedContent RenderSection(string name, bool required)
{
var renderTask = RenderSectionAsync(name, required);

return renderTask.IsCompleted
? renderTask.GetAwaiter().GetResult()
: Task.Run(async () => await renderTask.ConfigureAwait(false)).GetAwaiter().GetResult();
: Task.Run(async () => await renderTask.ConfigureAwait(false), CancellationToken.None).GetAwaiter().GetResult();
}

/// <summary>
/// Renders a required section asynchronously and returns the result as encoded content.
/// </summary>
/// <param name="name">The section name.</param>
/// <returns>The content to write to the output.</returns>
protected Task<IEncodedContent> RenderSectionAsync(string name)
protected internal Task<IEncodedContent> RenderSectionAsync(string name)
=> RenderSectionAsync(name, true);

/// <summary>
Expand All @@ -89,29 +83,16 @@ protected Task<IEncodedContent> RenderSectionAsync(string name)
/// <param name="name">The section name.</param>
/// <param name="required">Whether the section is required.</param>
/// <returns>The content to write to the output.</returns>
protected async Task<IEncodedContent> RenderSectionAsync(string name, bool required)
protected internal async Task<IEncodedContent> RenderSectionAsync(string name, bool required)
{
if (!LayoutInput.Sections.TryGetValue(name, out var sectionAction))
{
if (required)
throw new InvalidOperationException($"Section '{name}' is not defined.");

return StringBuilderEncodedContent.Empty;
}
var result = await LayoutInput.RenderSectionAsync(name).ConfigureAwait(false);

var previousOutput = Output;
if (result is not null)
return result;

try
{
var stringWriter = new StringWriter();
Output = stringWriter;
if (required)
throw new InvalidOperationException($"Section '{name}' is not defined.");

await sectionAction.Invoke().ConfigureAwait(false);
return new StringBuilderEncodedContent(stringWriter.GetStringBuilder());
}
finally
{
Output = previousOutput;
}
return StringBuilderEncodedContent.Empty;
}
}
16 changes: 8 additions & 8 deletions src/RazorBlade.Library/IRazorLayout.cs
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;

namespace RazorBlade;
Expand All @@ -15,7 +13,7 @@ public interface IRazorLayout
/// </summary>
/// <param name="input">The input data.</param>
/// <returns>The output data after rendering the layout, which can be used for the next layout.</returns>
Task<IExecutionResult> RenderLayoutAsync(IExecutionResult input);
Task<IExecutionResult> ExecuteLayoutAsync(IExecutionResult input);

/// <summary>
/// The execution result of a page.
Expand All @@ -33,13 +31,15 @@ public interface IExecutionResult
IRazorLayout? Layout { get; }

/// <summary>
/// The sections this page has defined.
/// The cancellation token.
/// </summary>
IReadOnlyDictionary<string, Func<Task>> Sections { get; }
CancellationToken CancellationToken { get; }

/// <summary>
/// The cancellation token.
/// Renders a section.
/// </summary>
CancellationToken CancellationToken { get; }
/// <param name="name">The section name.</param>
/// <returns>The rendered output, or null if the section is not defined.</returns>
Task<IEncodedContent?> RenderSectionAsync(string name);
}
}
131 changes: 79 additions & 52 deletions src/RazorBlade.Library/RazorTemplate.cs
Expand Up @@ -15,7 +15,9 @@ namespace RazorBlade;
/// </summary>
public abstract class RazorTemplate : IEncodedContent
{
private protected readonly Dictionary<string, Func<Task>> _sections = new(StringComparer.OrdinalIgnoreCase);
private Dictionary<string, Func<Task>>? _sections;

private Dictionary<string, Func<Task>> Sections => _sections ??= new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// The <see cref="TextWriter"/> which receives the output.
Expand Down Expand Up @@ -44,11 +46,11 @@ public string Render(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

var renderTask = RenderAsync(cancellationToken);
var renderTask = RenderAsyncCore(cancellationToken);
if (renderTask.IsCompleted)
return renderTask.Result;
return renderTask.GetAwaiter().GetResult().ToString();

return Task.Run(async () => await renderTask.ConfigureAwait(false), CancellationToken.None).GetAwaiter().GetResult();
return Task.Run(async () => await renderTask.ConfigureAwait(false), CancellationToken.None).GetAwaiter().GetResult().ToString();
}

/// <summary>
Expand Down Expand Up @@ -85,9 +87,8 @@ public async Task<string> RenderAsync(CancellationToken cancellationToken = defa
{
cancellationToken.ThrowIfCancellationRequested();

var output = new StringWriter();
await RenderAsync(output, cancellationToken).ConfigureAwait(false);
return output.ToString();
var stringBuilder = await RenderAsyncCore(cancellationToken).ConfigureAwait(false);
return stringBuilder.ToString();
}

/// <summary>
Expand All @@ -102,58 +103,53 @@ public async Task RenderAsync(TextWriter textWriter, CancellationToken cancellat
{
cancellationToken.ThrowIfCancellationRequested();

var previousState = (Output, CancellationToken);
var stringBuilder = await RenderAsyncCore(cancellationToken).ConfigureAwait(false);

#if NET6_0_OR_GREATER
await textWriter.WriteAsync(stringBuilder, cancellationToken).ConfigureAwait(false);
#else
await textWriter.WriteAsync(stringBuilder.ToString()).ConfigureAwait(false);
#endif
}

private async Task<StringBuilder> RenderAsyncCore(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var previousState = (_sections, Output, CancellationToken, Layout);

try
{
var stringWriter = new StringWriter();
var output = new StringWriter();

Output = stringWriter;
_sections = null;
Output = output;
CancellationToken = cancellationToken;
Layout = null;

await ExecuteAsync().ConfigureAwait(false);

if (Layout is null)
return output.GetStringBuilder();

IRazorLayout.IExecutionResult executionResult = new ExecutionResult(this, output.GetStringBuilder());

while (executionResult.Layout is { } layout)
{
#if NET6_0_OR_GREATER
await textWriter.WriteAsync(stringWriter.GetStringBuilder(), cancellationToken).ConfigureAwait(false);
#else
await textWriter.WriteAsync(stringWriter.ToString()).ConfigureAwait(false);
#endif
}
else
{
IRazorLayout.IExecutionResult executionResult = new ExecutionResult
{
Body = new StringBuilderEncodedContent(stringWriter.GetStringBuilder()),
Layout = Layout,
Sections = _sections,
CancellationToken = CancellationToken
};

while (executionResult.Layout is { } layout)
{
CancellationToken.ThrowIfCancellationRequested();
executionResult = await layout.RenderLayoutAsync(executionResult).ConfigureAwait(false);
}

if (executionResult.Body is StringBuilderEncodedContent { StringBuilder: var resultStringBuilder })
{
#if NET6_0_OR_GREATER
await textWriter.WriteAsync(resultStringBuilder, cancellationToken).ConfigureAwait(false);
#else
await textWriter.WriteAsync(resultStringBuilder.ToString()).ConfigureAwait(false);
#endif
}
else
{
executionResult.Body.WriteTo(textWriter);
}
cancellationToken.ThrowIfCancellationRequested();
executionResult = await layout.ExecuteLayoutAsync(executionResult).ConfigureAwait(false);
}

if (executionResult.Body is StringBuilderEncodedContent { StringBuilder: var outputWithLayout })
return outputWithLayout;

var outerBodyResult = new StringWriter();
executionResult.Body.WriteTo(outerBodyResult);
return outerBodyResult.GetStringBuilder();
}
finally
{
(Output, CancellationToken) = previousState;
(_sections, Output, CancellationToken, Layout) = previousState;
}
}

Expand Down Expand Up @@ -229,13 +225,13 @@ protected internal virtual void Write(IEncodedContent? content)
protected internal void DefineSection(string name, Func<Task> action)
{
#if NET6_0_OR_GREATER
if (!_sections.TryAdd(name, action))
if (!Sections.TryAdd(name, action))
throw new InvalidOperationException($"Section '{name}' is already defined.");
#else
if (_sections.ContainsKey(name))
if (Sections.ContainsKey(name))
throw new InvalidOperationException($"Section '{name}' is already defined.");

_sections[name] = action;
Sections[name] = action;
#endif
}

Expand All @@ -244,10 +240,41 @@ void IEncodedContent.WriteTo(TextWriter textWriter)

private protected class ExecutionResult : IRazorLayout.IExecutionResult
{
public IEncodedContent Body { get; set; } = null!;
public IRazorLayout? Layout { get; set; }
public IReadOnlyDictionary<string, Func<Task>> Sections { get; set; } = null!;
public CancellationToken CancellationToken { get; set; }
private readonly RazorTemplate _page;

public IEncodedContent Body { get; }
public IRazorLayout? Layout { get; }
public CancellationToken CancellationToken { get; }

public ExecutionResult(RazorTemplate page, StringBuilder body)
{
_page = page;
Body = new StringBuilderEncodedContent(body);
Layout = page.Layout;
CancellationToken = page.CancellationToken;
}

public async Task<IEncodedContent?> RenderSectionAsync(string name)
{
if (!_page.Sections.TryGetValue(name, out var sectionAction))
return null;

var previousOutput = _page.Output;

try
{
var output = new StringWriter();
_page.Output = output;

await sectionAction().ConfigureAwait(false);

return new StringBuilderEncodedContent(output.GetStringBuilder());
}
finally
{
_page.Output = previousOutput;
}
}
}

private protected class StringBuilderEncodedContent : IEncodedContent
Expand Down

0 comments on commit 3e8cbdc

Please sign in to comment.