feat: working on making handlebars a little less painful

This commit is contained in:
Dylan R. E. Moonfire 2022-07-09 19:24:55 -05:00
parent 858080f5fb
commit 88dc0e6fe7
11 changed files with 249 additions and 74 deletions

View file

@ -15,18 +15,18 @@ namespace Nitride.Handlebars;
/// An operation that applies a common or shared template on the content of
/// a document that includes theme or styling information.
/// </summary>
public class ApplyStyleTemplate<TModel> : OperationBase
[WithProperties]
public partial class ApplyStyleTemplate : OperationBase
{
// TODO: This does not use [WithProperties] because the source generator hasn't been taught how to do generics.
private readonly HandlebarsTemplateCache cache;
private readonly IValidator<ApplyStyleTemplate<TModel>> validator;
private readonly IValidator<ApplyStyleTemplate> validator;
public ApplyStyleTemplate(HandlebarsTemplateCache cache)
public ApplyStyleTemplate(
IValidator<ApplyStyleTemplate> validator,
HandlebarsTemplateCache cache)
{
// TODO: Figure out why Autofac won't let us register IValidator of generic classes.
this.validator = new ApplyStyleTemplateValidator<TModel>();
this.validator = validator;
this.cache = cache;
}
@ -35,7 +35,7 @@ public class ApplyStyleTemplate<TModel> : OperationBase
/// entity. This allows for the website to customize what information is
/// being passed to the template.
/// </summary>
public Func<Entity, TModel>? CreateModelCallback { get; set; }
public Func<Entity, object>? CreateModelCallback { get; set; }
/// <summary>
/// Gets or sets the callback used to determine which template to use
@ -55,38 +55,11 @@ public class ApplyStyleTemplate<TModel> : OperationBase
return input.SelectEntity<ITextContent>(this.Apply);
}
/// <summary>
/// Sets the callback for the template and returns the operation to
/// chain operations.
/// </summary>
/// <param name="callback">The callback to set.</param>
/// <returns>The ApplyContentHandlebarsTemplate to chain requests.</returns>
public ApplyStyleTemplate<TModel> WithCreateModelCallback(Func<Entity, TModel>? callback)
{
this.CreateModelCallback = callback;
return this;
}
public ApplyStyleTemplate<TModel> WithGetTemplateName(Func<Entity, string>? callback)
{
this.GetTemplateName = callback;
return this;
}
public ApplyStyleTemplate<TModel> WithHandlebars(IHandlebars? handlebars)
{
this.Handlebars = handlebars;
return this;
}
private Entity Apply(
Entity entity,
ITextContent content)
{
TModel model = this.CreateModelCallback!(entity);
object model = this.CreateModelCallback!(entity);
string name = this.GetTemplateName!(entity);
HandlebarsTemplate<object, object> template = this.cache.GetNamedTemplate(name);
string result = template(model!);

View file

@ -2,7 +2,7 @@ using FluentValidation;
namespace Nitride.Handlebars;
public class ApplyStyleTemplateValidator<TModel> : AbstractValidator<ApplyStyleTemplate<TModel>>
public class ApplyStyleTemplateValidator : AbstractValidator<ApplyStyleTemplate>
{
public ApplyStyleTemplateValidator()
{

View file

@ -0,0 +1,35 @@
using System.IO;
using HandlebarsDotNet;
namespace Nitride.Handlebars.Configuration;
/// <summary>
/// Loads the templates from the given directory.
/// </summary>
public class FileSystemHandlebarsTemplateLoader : IHandlebarsLoader
{
private readonly DirectoryInfo directory;
private readonly string pattern;
public FileSystemHandlebarsTemplateLoader(
DirectoryInfo directory,
string pattern = "*.hbs")
{
this.directory = directory;
this.pattern = pattern;
}
/// <inheritdoc />
public void Register(IHandlebars handlebars)
{
foreach (FileInfo file in this.directory.GetFiles(this.pattern))
{
string name = Path.GetFileNameWithoutExtension(file.Name);
string content = File.ReadAllText(file.FullName);
handlebars.RegisterTemplate(name, content);
}
}
}

View file

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using HandlebarsDotNet;
namespace Nitride.Handlebars.Configuration;
/// <summary>
/// </summary>
/// <typeparam name="TModel">A typesafe model of the template.</typeparam>
public class ForEachHandlebarsBlock<TModel> : HandlebarsBlockBase
{
public ForEachHandlebarsBlock(string helperName)
{
this.HelperName = helperName;
}
/// <summary>
/// Gets or sets the list that needs to be rendered.
/// </summary>
public Func<TModel, IEnumerable<object>>? GetList { get; set; }
/// <summary>
/// Gets or sets the callback that is called when nothing is found.
/// </summary>
public Action<EncodedTextWriter, BlockHelperOptions, Context, Arguments>? NothingFound { get; set; }
/// <inheritdoc />
protected override string HelperName { get; }
public ForEachHandlebarsBlock<TModel> WithGetList(Func<TModel, IEnumerable<object>>? callback)
{
this.GetList = callback;
return this;
}
public ForEachHandlebarsBlock<TModel> WithNothingFoundText(
Action<EncodedTextWriter, BlockHelperOptions, Context, Arguments>? callback)
{
this.NothingFound = callback;
return this;
}
/// <summary>
/// Sets the output to write out text when no items are found.
/// </summary>
/// <param name="text">The text to write out.</param>
/// <returns>The same object for chained methods.</returns>
public ForEachHandlebarsBlock<TModel> WithNothingFoundText(string text)
{
this.NothingFound = (
output,
_1,
_2,
_3) => output.Write(text);
return this;
}
/// <inheritdoc />
protected override void Render(
EncodedTextWriter output,
BlockHelperOptions options,
Context context,
Arguments input)
{
if (context.Value is not TModel model)
{
throw new InvalidOperationException(
string.Format(
"Cannot apply the {0} on context value because it is {1}.",
nameof(ForEachHandlebarsBlock<TModel>),
context.Value.GetType()));
}
IEnumerable<object>? list = this.GetList?.Invoke(model);
bool hasItems = false;
if (list != null)
{
foreach (object item in list)
{
hasItems = true;
options.Template(output, item);
}
}
if (!hasItems)
{
this.NothingFound?.Invoke(output, options, context, input);
}
}
}

View file

@ -0,0 +1,30 @@
using HandlebarsDotNet;
namespace Nitride.Handlebars.Configuration;
/// <summary>
/// Describes a block helper which can be registered.
/// </summary>
public abstract class HandlebarsBlockBase : IHandlebarsLoader
{
/// <summary>
/// Gets the name of the helper, which is how it is called in the template.
/// </summary>
protected abstract string HelperName { get; }
public void Register(IHandlebars handlebars)
{
HandlebarsBlockHelper blockHelper = this.Render;
handlebars.RegisterHelper(this.HelperName, blockHelper);
}
/// <summary>
/// Renders the helper to the template.
/// </summary>
protected abstract void Render(
EncodedTextWriter output,
BlockHelperOptions options,
Context context,
Arguments input);
}

View file

@ -0,0 +1,15 @@
using HandlebarsDotNet;
namespace Nitride.Handlebars.Configuration;
/// <summary>
/// Describes a dependency injected loader of templates or modules.
/// </summary>
public interface IHandlebarsLoader
{
/// <summary>
/// Registers the given helper into the handlebars engine.
/// </summary>
/// <param name="handlebars">The handlebars to register the helper into.</param>
void Register(IHandlebars handlebars);
}

View file

@ -1,11 +1,39 @@
using System;
using System.Collections.Generic;
using Autofac;
using Nitride.Handlebars.Configuration;
namespace Nitride.Handlebars;
public static class NitrideHandlebarsBuilderExtensions
{
public static NitrideBuilder UseHandlebars(this NitrideBuilder builder)
{
return builder.ConfigureContainer(x => x.RegisterModule<NitrideHandlebarsModule>());
return builder
.ConfigureContainer(x => x.RegisterModule<NitrideHandlebarsModule>());
}
public static NitrideBuilder UseHandlebars(
this NitrideBuilder builder,
Func<ContainerBuilder, IEnumerable<IHandlebarsLoader>> configure)
{
builder.UseHandlebars();
builder.ConfigureContainer(
c =>
{
IEnumerable<IHandlebarsLoader> loaders = configure(c);
foreach (IHandlebarsLoader loader in loaders)
{
c.RegisterInstance(loader)
.As<IHandlebarsLoader>()
.SingleInstance();
}
});
return builder;
}
}

View file

@ -1,5 +1,11 @@
using System.Collections.Generic;
using Autofac;
using HandlebarsDotNet;
using Nitride.Handlebars.Configuration;
namespace Nitride.Handlebars;
public class NitrideHandlebarsModule : Module
@ -14,10 +20,20 @@ public class NitrideHandlebarsModule : Module
.AsSelf()
.SingleInstance();
builder.RegisterGeneric(typeof(RenderContentTemplate<>))
.As(typeof(RenderContentTemplate<>));
builder.Register(
(context) =>
{
IHandlebars handlebars = HandlebarsDotNet.Handlebars.Create();
IEnumerable<IHandlebarsLoader> helpers = context.Resolve<IEnumerable<IHandlebarsLoader>>();
builder.RegisterGeneric(typeof(ApplyStyleTemplate<>))
.As(typeof(ApplyStyleTemplate<>));
foreach (IHandlebarsLoader helper in helpers)
{
helper.Register(handlebars);
}
return handlebars;
})
.As<IHandlebars>()
.SingleInstance();
}
}

View file

@ -16,18 +16,18 @@ namespace Nitride.Handlebars;
/// applied against the content and metadata and then replaces the content
/// of that entity.
/// </summary>
public class RenderContentTemplate<TModel> : OperationBase
[WithProperties]
public partial class RenderContentTemplate : OperationBase
{
private readonly HandlebarsTemplateCache cache;
// TODO: This does not use [WithProperties] because the source generator hasn't been taught how to do generics.
private readonly IValidator<RenderContentTemplate> validator;
private readonly IValidator<RenderContentTemplate<TModel>> validator;
public RenderContentTemplate(HandlebarsTemplateCache cache)
public RenderContentTemplate(
IValidator<RenderContentTemplate> validator,
HandlebarsTemplateCache cache)
{
// TODO: Figure out why Autofac won't let us register IValidator of generic classes.
this.validator = new RenderContentTemplateValidator<TModel>();
this.validator = validator;
this.cache = cache;
}
@ -36,7 +36,7 @@ public class RenderContentTemplate<TModel> : OperationBase
/// entity. This allows for the website to customize what information is
/// being passed to the template.
/// </summary>
public Func<Entity, TModel>? CreateModelCallback { get; set; }
public Func<Entity, object>? CreateModelCallback { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
@ -46,36 +46,18 @@ public class RenderContentTemplate<TModel> : OperationBase
return input.SelectEntity<HasHandlebarsTemplate, ITextContent>(this.Apply);
}
/// <summary>
/// Sets the callback for the template and returns the operation to
/// chain operations.
/// </summary>
/// <param name="callback">The callback to set.</param>
/// <returns>The ApplyContentHandlebarsTemplate to chain requests.</returns>
public RenderContentTemplate<TModel> WithCreateModelCallback(Func<Entity, TModel>? callback)
{
this.CreateModelCallback = callback;
return this;
}
private Entity Apply(
Entity entity,
HasHandlebarsTemplate _,
ITextContent content)
{
// Create the model using the callback.
TModel model = this.CreateModelCallback!(entity);
// Create a template from the contents.
string text = content.GetText();
HandlebarsTemplate<object, object> template = this.cache.GetLiteralTemplate(text);
// Render the template and create a new entity with the updated
// text.
object model = this.CreateModelCallback!(entity);
string result = template(model!);
return entity.Remove<HasHandlebarsTemplate>()
.SetTextContent(new StringTextContent(result));
return entity
.Remove<HasHandlebarsTemplate>()
.SetTextContent(result);
}
}

View file

@ -2,7 +2,7 @@ using FluentValidation;
namespace Nitride.Handlebars;
public class RenderContentTemplateValidator<TModel> : AbstractValidator<RenderContentTemplate<TModel>>
public class RenderContentTemplateValidator : AbstractValidator<RenderContentTemplate>
{
public RenderContentTemplateValidator()
{

View file

@ -92,7 +92,8 @@ public class NitrideBuilder
"Application name must be set, such as with the NitrideBuilder.WithApplicationName() to properly run.");
}
return await ToolBoxBuilder.Create(this.nitrideModule.ApplicationName, this.arguments)
return await ToolBoxBuilder
.Create(this.nitrideModule.ApplicationName, this.arguments)
.ConfigureContainer(this.ConfigureContainer)
.Build()
.RunAsync();