Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Fluid.Core" Version="1.0.0" />
<PackageReference Include="Fluid.Core" Version="2.0.13" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
</ItemGroup>

Expand Down
17 changes: 0 additions & 17 deletions src/Renderers/FluentEmail.Liquid/FluidViewTemplate.cs

This file was deleted.

84 changes: 84 additions & 0 deletions src/Renderers/FluentEmail.Liquid/LiquidParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Fluid;
using Fluid.Ast;

public class LiquidParser : FluidParser
{
public LiquidParser()
{
RegisterExpressionTag("layout", OnRegisterLayoutTag);
RegisterEmptyTag("renderbody", OnRegisterRenderBodyTag);
RegisterIdentifierBlock("section", OnRegisterSectionBlock);
RegisterIdentifierTag("rendersection", OnRegisterSectionTag);
}

private async ValueTask<Completion> OnRegisterLayoutTag(Expression expression, TextWriter writer, TextEncoder encoder, TemplateContext context)
{
const string viewExtension = ".liquid";

var relativeLayoutPath = (await expression.EvaluateAsync(context)).ToStringValue();

if (!relativeLayoutPath.EndsWith(viewExtension, StringComparison.OrdinalIgnoreCase))
{
relativeLayoutPath += viewExtension;
}

context.AmbientValues["Layout"] = relativeLayoutPath;

return Completion.Normal;
}

private async ValueTask<Completion> OnRegisterRenderBodyTag(TextWriter writer, TextEncoder encoder, TemplateContext context)
{
static void ThrowParseException()
{
throw new ParseException("Could not render body, Layouts can't be evaluated directly.");
}

if (context.AmbientValues.TryGetValue("Body", out var body))
{
await writer.WriteAsync((string)body);
}
else
{
ThrowParseException();
}

return Completion.Normal;
}

private ValueTask<Completion> OnRegisterSectionBlock(string sectionName, IReadOnlyList<Statement> statements, TextWriter writer, TextEncoder encoder, TemplateContext context)
{
if (context.AmbientValues.TryGetValue("Sections", out var sections))
{
var dictionary = (Dictionary<string, List<Statement>>) sections;

dictionary[sectionName] = statements.ToList();
}

return new ValueTask<Completion>(Completion.Normal);
}

private async ValueTask<Completion> OnRegisterSectionTag(string sectionName, TextWriter writer, TextEncoder encoder, TemplateContext context)
{
if (context.AmbientValues.TryGetValue("Sections", out var sections))
{
var dictionary = (Dictionary<string, List<Statement>>) sections;

if (dictionary.TryGetValue(sectionName, out var section))
{
foreach(var statement in section)
{
await statement.WriteToAsync(writer, encoder, context);
}
}
}

return Completion.Normal;
}
}
21 changes: 11 additions & 10 deletions src/Renderers/FluentEmail.Liquid/LiquidRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ namespace FluentEmail.Liquid
{
public class LiquidRenderer : ITemplateRenderer
{
private static readonly Func<IFluidTemplate> FluidTemplateFactory = () => new FluidViewTemplate();

private readonly IOptions<LiquidRendererOptions> _options;
private readonly LiquidParser _parser;

public LiquidRenderer(IOptions<LiquidRendererOptions> options)
{
_options = options;
_parser = new LiquidParser();
}

public string Parse<T>(string template, T model, bool isHtml = true)
Expand All @@ -34,17 +34,18 @@ public async Task<string> ParseAsync<T>(string template, T model, bool isHtml =
var fileProvider = rendererOptions.FileProvider;
var viewTemplate = ParseTemplate(template);

var context = new TemplateContext(model)
var context = new TemplateContext(model, rendererOptions.TemplateOptions)
{
// provide some services to all statements
AmbientValues =
{
["FileProvider"] = fileProvider,
["Sections"] = new Dictionary<string, List<Statement>>()
},
ParserFactory = FluidViewTemplate.Factory,
TemplateFactory = FluidTemplateFactory,
FileProvider = fileProvider
Options =
{
FileProvider = fileProvider
}
};

rendererOptions.ConfigureTemplateContext?.Invoke(context, model!);
Expand All @@ -55,15 +56,15 @@ public async Task<string> ParseAsync<T>(string template, T model, bool isHtml =
if (context.AmbientValues.TryGetValue("Layout", out var layoutPath))
{
context.AmbientValues["Body"] = body;
var layoutTemplate = ParseLiquidFile((string) layoutPath, fileProvider!);
var layoutTemplate = ParseLiquidFile((string)layoutPath, fileProvider!);

return await layoutTemplate.RenderAsync(context, rendererOptions.TextEncoder);
}

return body;
}

private static FluidViewTemplate ParseLiquidFile(
private IFluidTemplate ParseLiquidFile(
string path,
IFileProvider? fileProvider)
{
Expand All @@ -85,9 +86,9 @@ static void ThrowMissingFileProviderException()
return ParseTemplate(sr.ReadToEnd());
}

private static FluidViewTemplate ParseTemplate(string content)
private IFluidTemplate ParseTemplate(string content)
{
if (!FluidViewTemplate.TryParse(content, out var template, out var errors))
if (!_parser.TryParse(content, out var template, out var errors))
{
throw new Exception(string.Join(Environment.NewLine, errors));
}
Expand Down
5 changes: 5 additions & 0 deletions src/Renderers/FluentEmail.Liquid/LiquidRendererOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,10 @@ public class LiquidRendererOptions
/// File provider to use, used when resolving references in templates, like master layout.
/// </summary>
public IFileProvider? FileProvider { get; set; }

/// <summary>
/// Set custom Template Options for Fluid
/// </summary>
public TemplateOptions TemplateOptions { get; set; } = new();
}
}
28 changes: 0 additions & 28 deletions src/Renderers/FluentEmail.Liquid/Tags/LayoutTag.cs

This file was deleted.

26 changes: 0 additions & 26 deletions src/Renderers/FluentEmail.Liquid/Tags/RegisterSectionBlock.cs

This file was deleted.

31 changes: 0 additions & 31 deletions src/Renderers/FluentEmail.Liquid/Tags/RenderBodyTag.cs

This file was deleted.

30 changes: 0 additions & 30 deletions src/Renderers/FluentEmail.Liquid/Tags/RenderSectionTag.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using FluentEmail.Core;
using Fluid;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using NUnit.Framework;

namespace FluentEmail.Liquid.Tests.ComplexModel
{
public class ComplexModelRenderTests
{
public ComplexModelRenderTests()
{
SetupRenderer();
}

[Test]
public void Can_Render_Complex_Model_Properties()
{
var model = new ParentModel
{
ParentName = new NameDetails { Firstname = "Luke", Surname = "Dinosaur" },
ChildrenNames = new List<NameDetails>
{
new NameDetails { Firstname = "ChildFirstA", Surname = "ChildLastA" },
new NameDetails { Firstname = "ChildFirstB", Surname = "ChildLastB" }
}
};

var expected = @"
Parent: Luke
Children:

* ChildFirstA ChildLastA
* ChildFirstB ChildLastB
";

var email = Email
.From(TestData.FromEmail)
.To(TestData.ToEmail)
.Subject(TestData.Subject)
.UsingTemplate(Template(), model);
email.Data.Body.Should().Be(expected);
}

private string Template()
{
return @"
Parent: {{ ParentName.Firstname }}
Children:
{% for Child in ChildrenNames %}
* {{ Child.Firstname }} {{ Child.Surname }}{% endfor %}
";
}

private static void SetupRenderer(
IFileProvider fileProvider = null,
Action<TemplateContext, object> configureTemplateContext = null)
{
var options = new LiquidRendererOptions
{
FileProvider = fileProvider,
ConfigureTemplateContext = configureTemplateContext,
TemplateOptions = new TemplateOptions { MemberAccessStrategy = new UnsafeMemberAccessStrategy() }
};
Email.DefaultRenderer = new LiquidRenderer(Options.Create(options));
}
}
}
Loading