diff --git a/src/RazorBlade.Library/HtmlLayout.cs b/src/RazorBlade.Library/HtmlLayout.cs
index 6abafcd..14d9981 100644
--- a/src/RazorBlade.Library/HtmlLayout.cs
+++ b/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;
@@ -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.RenderLayoutAsync(IRazorLayout.IExecutionResult input)
+ async Task IRazorLayout.ExecuteLayoutAsync(IRazorLayout.IExecutionResult input)
{
input.CancellationToken.ThrowIfCancellationRequested();
var previousStatus = (Output, CancellationToken);
@@ -24,20 +23,15 @@ public abstract class HtmlLayout : HtmlTemplate, IRazorLayout
{
_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
{
@@ -49,7 +43,7 @@ public abstract class HtmlLayout : HtmlTemplate, IRazorLayout
///
/// Returns the inner page body.
///
- protected IEncodedContent RenderBody()
+ protected internal IEncodedContent RenderBody()
=> LayoutInput.Body;
///
@@ -57,7 +51,7 @@ protected IEncodedContent RenderBody()
///
/// The section name.
/// The content to write to the output.
- protected IEncodedContent RenderSection(string name)
+ protected internal IEncodedContent RenderSection(string name)
=> RenderSection(name, true);
///
@@ -66,13 +60,13 @@ protected IEncodedContent RenderSection(string name)
/// The section name.
/// Whether the section is required.
/// The content to write to the output.
- 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();
}
///
@@ -80,7 +74,7 @@ protected IEncodedContent RenderSection(string name, bool required)
///
/// The section name.
/// The content to write to the output.
- protected Task RenderSectionAsync(string name)
+ protected internal Task RenderSectionAsync(string name)
=> RenderSectionAsync(name, true);
///
@@ -89,29 +83,16 @@ protected Task RenderSectionAsync(string name)
/// The section name.
/// Whether the section is required.
/// The content to write to the output.
- protected async Task RenderSectionAsync(string name, bool required)
+ protected internal async Task 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;
}
}
diff --git a/src/RazorBlade.Library/IRazorLayout.cs b/src/RazorBlade.Library/IRazorLayout.cs
index 915938f..8b38879 100644
--- a/src/RazorBlade.Library/IRazorLayout.cs
+++ b/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;
@@ -15,7 +13,7 @@ public interface IRazorLayout
///
/// The input data.
/// The output data after rendering the layout, which can be used for the next layout.
- Task RenderLayoutAsync(IExecutionResult input);
+ Task ExecuteLayoutAsync(IExecutionResult input);
///
/// The execution result of a page.
@@ -33,13 +31,15 @@ public interface IExecutionResult
IRazorLayout? Layout { get; }
///
- /// The sections this page has defined.
+ /// The cancellation token.
///
- IReadOnlyDictionary> Sections { get; }
+ CancellationToken CancellationToken { get; }
///
- /// The cancellation token.
+ /// Renders a section.
///
- CancellationToken CancellationToken { get; }
+ /// The section name.
+ /// The rendered output, or null if the section is not defined.
+ Task RenderSectionAsync(string name);
}
}
diff --git a/src/RazorBlade.Library/RazorTemplate.cs b/src/RazorBlade.Library/RazorTemplate.cs
index 48c6995..87b7218 100644
--- a/src/RazorBlade.Library/RazorTemplate.cs
+++ b/src/RazorBlade.Library/RazorTemplate.cs
@@ -15,7 +15,9 @@ namespace RazorBlade;
///
public abstract class RazorTemplate : IEncodedContent
{
- private protected readonly Dictionary> _sections = new(StringComparer.OrdinalIgnoreCase);
+ private Dictionary>? _sections;
+
+ private Dictionary> Sections => _sections ??= new(StringComparer.OrdinalIgnoreCase);
///
/// The which receives the output.
@@ -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();
}
///
@@ -85,9 +87,8 @@ public async Task 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();
}
///
@@ -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 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;
}
}
@@ -229,13 +225,13 @@ protected internal virtual void Write(IEncodedContent? content)
protected internal void DefineSection(string name, Func 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
}
@@ -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> 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 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
diff --git a/src/RazorBlade.Tests/HtmlLayoutTests.cs b/src/RazorBlade.Tests/HtmlLayoutTests.cs
new file mode 100644
index 0000000..99c4fb9
--- /dev/null
+++ b/src/RazorBlade.Tests/HtmlLayoutTests.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using RazorBlade.Tests.Support;
+
+namespace RazorBlade.Tests;
+
+[TestFixture]
+public class HtmlLayoutTests
+{
+ [Test]
+ public void should_render_layout()
+ {
+ var layout = new Layout(t =>
+ {
+ t.Write("before ");
+ t.Write(t.RenderBody());
+ t.Write(" after");
+ });
+
+ var page = new Template(t =>
+ {
+ t.Write("body");
+ t.Layout = layout;
+ });
+
+ page.Render().ShouldEqual("before body after");
+ }
+
+ [Test]
+ public void should_render_sections()
+ {
+ var layout = new Layout(t =>
+ {
+ t.Write("before ");
+ t.Write(t.RenderSection("foo"));
+ t.Write(" after");
+ });
+
+ var page = new Template(t =>
+ {
+ t.Layout = layout;
+ t.SetSection("foo", "foo section");
+ });
+
+ page.Render().ShouldEqual("before foo section after");
+ }
+
+ [Test]
+ public void should_render_nested_layouts()
+ {
+ var outerLayout = new Layout(t =>
+ {
+ t.Write("beforeA ");
+ t.Write(t.RenderBody());
+ t.Write(" afterA");
+ });
+
+ var innerLayout = new Layout(t =>
+ {
+ t.Write("beforeB ");
+ t.Write(t.RenderBody());
+ t.Write(" afterB");
+ t.Layout = outerLayout;
+ });
+
+ var page = new Template(t =>
+ {
+ t.Write("body");
+ t.Layout = innerLayout;
+ });
+
+ page.Render().ShouldEqual("beforeA beforeB body afterB afterA");
+ }
+
+ [Test]
+ public void should_render_nested_layout_sections()
+ {
+ var outerLayout = new Layout(t =>
+ {
+ t.Write(t.RenderSection("inner"));
+ t.Write(t.RenderSection("inner2", false));
+ t.SetSection("outer", "outerSection ");
+ t.Write(t.RenderSection("outer", false));
+ t.Write(t.RenderBody());
+ });
+
+ var innerLayout = new Layout(t =>
+ {
+ t.Write(t.RenderSection("page"));
+ t.Write(t.RenderSection("page2", false));
+ t.SetSection("inner", "innerSection ");
+ t.Write(t.RenderSection("inner", false));
+ t.Write(t.RenderBody());
+ t.Layout = outerLayout;
+ });
+
+ var page = new Template(t =>
+ {
+ t.SetSection("page", "pageSection");
+ t.Layout = innerLayout;
+ });
+
+ page.Render().ShouldEqual("innerSection pageSection");
+ }
+
+ private class Template : HtmlTemplate
+ {
+ private readonly Action _executeAction;
+
+ public Template(Action executeAction)
+ => _executeAction = executeAction;
+
+ protected internal override Task ExecuteAsync()
+ {
+ _executeAction(this);
+ return base.ExecuteAsync();
+ }
+
+ public void SetSection(string name, string content)
+ {
+ DefineSection(
+ name,
+ () =>
+ {
+ Write(content);
+ return Task.CompletedTask;
+ }
+ );
+ }
+ }
+
+ private class Layout : HtmlLayout
+ {
+ private readonly Action _executeAction;
+
+ public Layout(Action executeAction)
+ => _executeAction = executeAction;
+
+ protected internal override Task ExecuteAsync()
+ {
+ _executeAction(this);
+ return base.ExecuteAsync();
+ }
+
+ public void SetSection(string name, string content)
+ {
+ DefineSection(
+ name,
+ () =>
+ {
+ Write(content);
+ return Task.CompletedTask;
+ }
+ );
+ }
+ }
+}