From ef4e7c36426382e6f8ae8ea1d97ef20e4864ebdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20L=2E=20Charlier?= Date: Sat, 19 Oct 2024 22:35:43 +0200 Subject: [PATCH] feat: Handlebars template engine (#12) --- README.md | 12 ++- src/Didot.Core/Didot.Core.csproj | 1 + .../FileBasedTemplateEngineFactory.cs | 1 + .../TemplateEngines/HandlebarsWrapper.cs | 24 ++++++ .../TemplateEngines/DotLiquidWrapperTests.cs | 16 ++-- .../FileBasedTemplateEngineFactoryTests.cs | 1 + .../TemplateEngines/HandlebarsWrapperTests.cs | 81 +++++++++++++++++++ .../TemplateEngines/ScribanWrapperTests.cs | 18 ++--- 8 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 src/Didot.Core/TemplateEngines/HandlebarsWrapper.cs create mode 100644 testing/Didot.Core.Testing/TemplateEngines/HandlebarsWrapperTests.cs diff --git a/README.md b/README.md index 15dc104..c176946 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ dotnet tool install -g Didot-cli ## QuickStart -**Didot** is a command-line tool designed for generating files based on templating. It supports *YAML*, *JSON*, and *XML* as source data formats and provides flexibility in templating through both *Scriban* and *DotLiquid* engines. With Didot, you can easily automate file generation by combining structured data from YAML, JSON, or XML files with customizable templates using Scriban or DotLiquid. +**Didot** is a command-line tool designed for generating files based on templating. It supports *YAML*, *JSON*, and *XML* as source data formats and provides flexibility in templating through both *Scriban*, *Liquid* and *Handlebars* templates languages. With Didot, you can easily automate file generation by combining structured data from YAML, JSON, or XML files with customizable templates using Scriban or DotLiquid. ### Supported Data Formats: @@ -59,6 +59,10 @@ Didot utilizes some templating engines, which allow for powerful and flexible te - Secure (no access to system objects), making it ideal for user-generated templates. - Allows both dynamic and static templating. - Supports filters, tags, and various control flow structures. +- **Handlebars**: Templates with the `.hbs` extension are parsed using a Handlebars template engine. Handlebars C# port of the popular JavaScript Handlebars templating engine. + - Simple syntax for generating HTML or text files from templates. + - Support for helpers, partial templates, and block helpers. + - Good separation of logic from presentation. ### Command Usage: @@ -76,8 +80,8 @@ didot -t template.scriban -s data.yaml -o page.html In this example: -* template.scriban is the Scriban template file. -* data.yaml is the source file containing the structured data in YAML format. -* result.txt is the output file that will contain the generated content. +* `template.scriban` is the Scriban template file. +* `data.yaml` is the source file containing the structured data in YAML format. +* `page.html` is the output file that will contain the generated content. Make sure that the template file and source file are correctly formatted and aligned with your data model to produce the desired result. diff --git a/src/Didot.Core/Didot.Core.csproj b/src/Didot.Core/Didot.Core.csproj index 3d6bddb..e29ef6b 100644 --- a/src/Didot.Core/Didot.Core.csproj +++ b/src/Didot.Core/Didot.Core.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Didot.Core/TemplateEngines/FileBasedTemplateEngineFactory.cs b/src/Didot.Core/TemplateEngines/FileBasedTemplateEngineFactory.cs index cb7eeb7..868281e 100644 --- a/src/Didot.Core/TemplateEngines/FileBasedTemplateEngineFactory.cs +++ b/src/Didot.Core/TemplateEngines/FileBasedTemplateEngineFactory.cs @@ -13,6 +13,7 @@ public ITemplateEngine GetTemplateEngine(string extension) { ".scriban" => new ScribanWrapper(), ".liquid" => new DotLiquidWrapper(), + ".hbs" => new HandlebarsWrapper(), _ => throw new NotSupportedException() }; } diff --git a/src/Didot.Core/TemplateEngines/HandlebarsWrapper.cs b/src/Didot.Core/TemplateEngines/HandlebarsWrapper.cs new file mode 100644 index 0000000..76b19ec --- /dev/null +++ b/src/Didot.Core/TemplateEngines/HandlebarsWrapper.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using HandlebarsDotNet; + +namespace Didot.Core.TemplateEngines; +public class HandlebarsWrapper : ITemplateEngine +{ + public string Render(string template, dynamic model) + { + var templateInstance = Handlebars.Compile(template); + return templateInstance(model); + } + + public string Render(Stream stream, dynamic model) + { + using var reader = new StreamReader(stream); + var template = reader.ReadToEnd(); + return Render(template, model); + } +} diff --git a/testing/Didot.Core.Testing/TemplateEngines/DotLiquidWrapperTests.cs b/testing/Didot.Core.Testing/TemplateEngines/DotLiquidWrapperTests.cs index 33fe27e..c6079c0 100644 --- a/testing/Didot.Core.Testing/TemplateEngines/DotLiquidWrapperTests.cs +++ b/testing/Didot.Core.Testing/TemplateEngines/DotLiquidWrapperTests.cs @@ -24,7 +24,7 @@ public void Render_SingleProperty_Successful() public void Render_MultiProperty_Successful() { var engine = new DotLiquidWrapper(); - var model = new Dictionary() + var model = new Dictionary() { { "Name", "Albert"}, {"Age", 30 } }; var result = engine.Render("Hello {{model.Name}}. You're {{model.Age}} years old.", new { model }); Assert.That(result, Is.EqualTo("Hello Albert. You're 30 years old.")); @@ -34,9 +34,9 @@ public void Render_MultiProperty_Successful() public void Render_NestedProperties_Successful() { var engine = new DotLiquidWrapper(); - var name = new Dictionary() + var name = new Dictionary() { { "First", "Albert"}, {"Last", "Einstein" } }; - var model = new Dictionary() + var model = new Dictionary() { { "Name", name}, {"Age", 30 } }; var result = engine.Render("Hello {{model.Name.First}} {{model.Name.Last}}. Your age is {{model.Age}} years old.", new { model }); Assert.That(result, Is.EqualTo("Hello Albert Einstein. Your age is 30 years old.")); @@ -46,9 +46,9 @@ public void Render_NestedProperties_Successful() public void Render_ArrayItems_Successful() { var engine = new DotLiquidWrapper(); - var albert = new Dictionary() + var albert = new Dictionary() { { "Name", "Albert"}, {"Age", 30 } }; - var nikola = new Dictionary() + var nikola = new Dictionary() { { "Name", "Nikola"}, {"Age", 50 } }; var model = new[] { albert, nikola }; var result = engine.Render("Hello {{model[0].Name}}. Your colleague is {{model[1].Age}} years old.", new { model }); @@ -59,9 +59,9 @@ public void Render_ArrayItems_Successful() public void Render_ArrayLoop_Successful() { var engine = new DotLiquidWrapper(); - var albert = new Dictionary() + var albert = new Dictionary() { { "Name", "Albert"}, {"Age", 30 } }; - var nikola = new Dictionary() + var nikola = new Dictionary() { { "Name", "Nikola"}, {"Age", 50 } }; var model = new[] { albert, nikola }; var result = engine.Render("Hello {% for item in model %}{{ item.Name }}{% if forloop.last == false %}, {% endif %}{% endfor %}!", new { model }); @@ -72,7 +72,7 @@ public void Render_ArrayLoop_Successful() public void Render_Stream_Successful() { var engine = new DotLiquidWrapper(); - var model = new Dictionary() + var model = new Dictionary() { { "Name", "World"} }; using var stream = new MemoryStream(Encoding.UTF8.GetBytes("Hello {{model.Name}}")); var result = engine.Render(stream, new { model }); diff --git a/testing/Didot.Core.Testing/TemplateEngines/FileBasedTemplateEngineFactoryTests.cs b/testing/Didot.Core.Testing/TemplateEngines/FileBasedTemplateEngineFactoryTests.cs index 4761284..7412cdc 100644 --- a/testing/Didot.Core.Testing/TemplateEngines/FileBasedTemplateEngineFactoryTests.cs +++ b/testing/Didot.Core.Testing/TemplateEngines/FileBasedTemplateEngineFactoryTests.cs @@ -12,6 +12,7 @@ public class FileBasedTemplateEngineFactoryTests [Test] [TestCase(".scriban", typeof(ScribanWrapper))] [TestCase(".liquid", typeof(DotLiquidWrapper))] + [TestCase(".hbs", typeof(HandlebarsWrapper))] public void GetSourceParser_Extension_CorrectParser(string extension, Type expected) { var factory = new FileBasedTemplateEngineFactory(); diff --git a/testing/Didot.Core.Testing/TemplateEngines/HandlebarsWrapperTests.cs b/testing/Didot.Core.Testing/TemplateEngines/HandlebarsWrapperTests.cs new file mode 100644 index 0000000..ad74f91 --- /dev/null +++ b/testing/Didot.Core.Testing/TemplateEngines/HandlebarsWrapperTests.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Didot.Core.TemplateEngines; +using NUnit.Framework; + +namespace Didot.Core.Testing.TemplateEngines; +public class HandlebarsWrapperTests +{ + [Test] + public void Render_SingleProperty_Successful() + { + var engine = new HandlebarsWrapper(); + var model = new Dictionary() + { { "Name", "World"} }; + var result = engine.Render("Hello {{model.Name}}", new { model }); + Assert.That(result, Is.EqualTo("Hello World")); + } + + [Test] + public void Render_MultiProperty_Successful() + { + var engine = new HandlebarsWrapper(); + var model = new Dictionary() + { { "Name", "Albert"}, {"Age", 30 } }; + var result = engine.Render("Hello {{model.Name}}. You're {{model.Age}} years old.", new { model }); + Assert.That(result, Is.EqualTo("Hello Albert. You're 30 years old.")); + } + + [Test] + public void Render_NestedProperties_Successful() + { + var engine = new HandlebarsWrapper(); + var name = new Dictionary() + { { "First", "Albert"}, {"Last", "Einstein" } }; + var model = new Dictionary() + { { "Name", name}, {"Age", 30 } }; + var result = engine.Render("{{#with model}}Hello {{#with Name}}{{First}} {{Last}}{{/with}}. Your age is {{Age}} years old.{{/with}}", new { model }); + Assert.That(result, Is.EqualTo("Hello Albert Einstein. Your age is 30 years old.")); + } + + [Test] + public void Render_ArrayItems_Successful() + { + var engine = new HandlebarsWrapper(); + var albert = new Dictionary() + { { "Name", "Albert"}, {"Age", 30 } }; + var nikola = new Dictionary() + { { "Name", "Nikola"}, {"Age", 50 } }; + var model = new[] { albert, nikola }; + var result = engine.Render("Hello {{model.0.Name}}. Your colleague is {{model.1.Age}} years old.", new { model }); + Assert.That(result, Is.EqualTo("Hello Albert. Your colleague is 50 years old.")); + } + + [Test] + public void Render_ArrayLoop_Successful() + { + var engine = new HandlebarsWrapper(); + var albert = new Dictionary() + { { "Name", "Albert"}, {"Age", 30 } }; + var nikola = new Dictionary() + { { "Name", "Nikola"}, {"Nikola", 50 } }; + var model = new[] { albert, nikola }; + var result = engine.Render("Hello {{#each model}}{{Name}}{{#unless @last}}, {{/unless}}{{/each}}!", new { model }); + Assert.That(result, Is.EqualTo("Hello Albert, Nikola!")); + } + + [Test] + public void Render_Stream_Successful() + { + var engine = new HandlebarsWrapper(); + var model = new Dictionary() + { { "Name", "World"} }; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes("Hello {{model.Name}}")); + var result = engine.Render(stream, new { model }); + Assert.That(result, Is.EqualTo("Hello World")); + } +} diff --git a/testing/Didot.Core.Testing/TemplateEngines/ScribanWrapperTests.cs b/testing/Didot.Core.Testing/TemplateEngines/ScribanWrapperTests.cs index f1a68ac..52b234e 100644 --- a/testing/Didot.Core.Testing/TemplateEngines/ScribanWrapperTests.cs +++ b/testing/Didot.Core.Testing/TemplateEngines/ScribanWrapperTests.cs @@ -14,7 +14,7 @@ public class ScribanWrapperTests public void Render_SingleProperty_Successful() { var engine = new ScribanWrapper(); - var model = new Dictionary() + var model = new Dictionary() { { "Name", "World"} }; var result = engine.Render("Hello {{model.Name}}", new { model }); Assert.That(result, Is.EqualTo("Hello World")); @@ -24,7 +24,7 @@ public void Render_SingleProperty_Successful() public void Render_MultiProperty_Successful() { var engine = new ScribanWrapper(); - var model = new Dictionary() + var model = new Dictionary() { { "Name", "Albert"}, {"Age", 30 } }; var result = engine.Render("Hello {{model.Name}}. You're {{model.Age}} years old.", new { model }); Assert.That(result, Is.EqualTo("Hello Albert. You're 30 years old.")); @@ -34,9 +34,9 @@ public void Render_MultiProperty_Successful() public void Render_NestedProperties_Successful() { var engine = new ScribanWrapper(); - var name = new Dictionary() + var name = new Dictionary() { { "First", "Albert"}, {"Last", "Einstein" } }; - var model = new Dictionary() + var model = new Dictionary() { { "Name", name}, {"Age", 30 } }; var result = engine.Render("Hello {{model.Name.First}} {{model.Name.Last}}. Your age is {{model.Age}} years old.", new { model }); Assert.That(result, Is.EqualTo("Hello Albert Einstein. Your age is 30 years old.")); @@ -46,9 +46,9 @@ public void Render_NestedProperties_Successful() public void Render_Array_Successful() { var engine = new ScribanWrapper(); - var albert = new Dictionary() + var albert = new Dictionary() { { "Name", "Albert"}, {"Age", 30 } }; - var nikola = new Dictionary() + var nikola = new Dictionary() { { "Name", "Nikola"}, {"Age", 50 } }; var model = new[] { albert, nikola }; var result = engine.Render("Hello {{model[0].Name}}. Your colleague is {{model[1].Age}} years old.", new { model }); @@ -59,9 +59,9 @@ public void Render_Array_Successful() public void Render_ArrayLoop_Successful() { var engine = new ScribanWrapper(); - var albert = new Dictionary() + var albert = new Dictionary() { { "Name", "Albert"}, {"Age", 30 } }; - var nikola = new Dictionary() + var nikola = new Dictionary() { { "Name", "Nikola"}, {"Age", 50 } }; var model = new[] { albert, nikola }; var result = engine.Render("Hello {{ for item in model; item.Name; if !for.last; \", \"; end; end }}!", new { model }); @@ -72,7 +72,7 @@ public void Render_ArrayLoop_Successful() public void Render_Stream_Successful() { var engine = new ScribanWrapper(); - var model = new Dictionary() + var model = new Dictionary() { { "Name", "World"} }; using var stream = new MemoryStream(Encoding.UTF8.GetBytes("Hello {{model.Name}}")); var result = engine.Render(stream, new { model });