Skip to content

Commit

Permalink
Layout WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ltrzesniewski committed Dec 1, 2023
1 parent 8a89e65 commit 162c1aa
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 3 deletions.
117 changes: 117 additions & 0 deletions src/RazorBlade.Library/HtmlLayout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace RazorBlade;

/// <summary>
/// Base class for HTML layout pages.
/// </summary>
public abstract class HtmlLayout : HtmlTemplate, IRazorLayout
{
private IRazorLayout.IExecutionResult? _layoutInput;

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

async Task<IRazorLayout.IExecutionResult> IRazorLayout.RenderLayoutAsync(IRazorLayout.IExecutionResult input)
{
input.CancellationToken.ThrowIfCancellationRequested();
var previousStatus = (Output, CancellationToken);

try
{
_layoutInput = input;

var stringWriter = new StringWriter();

Output = stringWriter;
CancellationToken = input.CancellationToken;

await ExecuteAsync().ConfigureAwait(false);

return new ExecutionResult
{
Body = new StringBuilderEncodedContent(stringWriter.GetStringBuilder()),
Layout = Layout,
Sections = _sections,
CancellationToken = CancellationToken
};
}
finally
{
_layoutInput = null;
(Output, CancellationToken) = previousStatus;
}
}

/// <summary>
/// Returns the inner page body.
/// </summary>
protected 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)
=> RenderSection(name, true);

/// <summary>
/// Renders a section and returns the result as encoded content.
/// </summary>
/// <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)
{
var renderTask = RenderSectionAsync(name, required);

return renderTask.IsCompleted
? renderTask.GetAwaiter().GetResult()
: Task.Run(async () => await renderTask.ConfigureAwait(false)).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)
=> RenderSectionAsync(name, true);

/// <summary>
/// Renders a section asynchronously and returns the result as encoded content.
/// </summary>
/// <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)
{
if (!LayoutInput.Sections.TryGetValue(name, out var sectionAction))
{
if (required)
throw new InvalidOperationException($"Section '{name}' is not defined.");

return StringBuilderEncodedContent.Empty;
}

var previousOutput = Output;

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

await sectionAction.Invoke().ConfigureAwait(false);
return new StringBuilderEncodedContent(stringWriter.GetStringBuilder());
}
finally
{
Output = previousOutput;
}
}
}
7 changes: 7 additions & 0 deletions src/RazorBlade.Library/HtmlTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ public abstract class HtmlTemplate : RazorTemplate
{
private AttributeInfo _currentAttribute;

/// <inheritdoc cref="RazorTemplate.Layout" />
protected internal new HtmlLayout? Layout
{
get => base.Layout as HtmlLayout;
set => base.Layout = value;
}

// ReSharper disable once RedundantDisableWarningComment
#pragma warning disable CA1822

Expand Down
45 changes: 45 additions & 0 deletions src/RazorBlade.Library/IRazorLayout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace RazorBlade;

/// <summary>
/// Represents a Razor layout page.
/// </summary>
public interface IRazorLayout
{
/// <summary>
/// Renders the layout for a given page.
/// </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);

/// <summary>
/// The execution result of a page.
/// </summary>
public interface IExecutionResult
{
/// <summary>
/// The rendered body contents.
/// </summary>
IEncodedContent Body { get; }

/// <summary>
/// The layout this execution result needs to be wrapped in.
/// </summary>
IRazorLayout? Layout { get; }

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

/// <summary>
/// The cancellation token.
/// </summary>
CancellationToken CancellationToken { get; }
}
}
76 changes: 73 additions & 3 deletions src/RazorBlade.Library/RazorTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
Expand All @@ -14,7 +15,7 @@ namespace RazorBlade;
/// </summary>
public abstract class RazorTemplate : IEncodedContent
{
private readonly Dictionary<string, Func<Task>> _sections = new(StringComparer.OrdinalIgnoreCase);
private protected readonly Dictionary<string, Func<Task>> _sections = new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// The <see cref="TextWriter"/> which receives the output.
Expand All @@ -24,7 +25,12 @@ public abstract class RazorTemplate : IEncodedContent
/// <summary>
/// The cancellation token.
/// </summary>
protected internal CancellationToken CancellationToken { get; private set; }
protected internal CancellationToken CancellationToken { get; set; }

/// <summary>
/// The layout to use.
/// </summary>
protected internal IRazorLayout? Layout { get; set; }

/// <summary>
/// Renders the template synchronously and returns the result as a string.
Expand Down Expand Up @@ -100,10 +106,50 @@ public async Task RenderAsync(TextWriter textWriter, CancellationToken cancellat

try
{
Output = textWriter;
var stringWriter = new StringWriter();

Output = stringWriter;
CancellationToken = cancellationToken;

await ExecuteAsync().ConfigureAwait(false);

if (Layout is null)
{
#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);
}
}
}
finally
{
Expand Down Expand Up @@ -195,4 +241,28 @@ protected internal void DefineSection(string name, Func<Task> action)

void IEncodedContent.WriteTo(TextWriter textWriter)
=> Render(textWriter, CancellationToken.None);

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 protected class StringBuilderEncodedContent : IEncodedContent
{
public static IEncodedContent Empty { get; } = new StringBuilderEncodedContent(new StringBuilder());

public StringBuilder StringBuilder { get; }

public StringBuilderEncodedContent(StringBuilder stringBuilder)
=> StringBuilder = stringBuilder;

public void WriteTo(TextWriter textWriter)
=> textWriter.Write(StringBuilder);

public override string ToString()
=> StringBuilder.ToString();
}
}

0 comments on commit 162c1aa

Please sign in to comment.