Skip to content

Commit 589f96e

Browse files
authored
feat: template engine builder (#175)
1 parent 0ebad2d commit 589f96e

13 files changed

+444
-4
lines changed

docs/_data/navigation_docs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- title: Features
2626
docs:
2727
- configuration
28+
- instantiate-engine
2829
- feature-formatter
2930
- feature-mappings
3031
- feature-partial

docs/_docs/instantiate-engine.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
title: Instantiating a template engine
3+
tags: [template, features]
4+
---
5+
## Overview
6+
7+
The `TemplateEngineBuilder` provides a unified and extensible way to instantiate a template engine of your choice (e.g., Handlebars, Fluid, Scriban, etc.), with optional configuration support.
8+
9+
This enables template rendering abstraction while preserving per-engine flexibility.
10+
11+
## How-to
12+
13+
```csharp
14+
var engine = new TemplateEngineBuilder()
15+
.UseScriban()
16+
.Build();
17+
18+
var output = engine.Render(templateSource, model);
19+
```
20+
21+
## Supported Engines
22+
23+
The following engines are available via the builder:
24+
25+
| Engine | Method |
26+
|----------------|-------------------------------|
27+
| Handlebars | `.UseHandlebars()` |
28+
| Fluid | `.UseFluid()` |
29+
| Scriban | `.UseScriban()` |
30+
| DotLiquid | `.UseDotLiquid()` |
31+
| Morestachio | `.UseMorestachio()` |
32+
| SmartFormat | `.UseSmartFormat()` |
33+
| StringTemplate | `.UseStringTemplate(...)` |
34+
35+
Each returns the builder instance for fluent chaining.
36+
37+
## Customizing the engine
38+
39+
To further tailor the behavior of certain engines, such as delimiter styles in StringTemplate, the builder supports optional configuration through customization methods.
40+
41+
### Customizing StringTemplate
42+
43+
`StringTemplate` supports configurable delimiters (either `<...>` or `$...$`). Use the overload with a lambda to specify this:
44+
45+
```csharp
46+
// Use angle brackets
47+
var engine = new TemplateEngineBuilder()
48+
.UseStringTemplate(opt => opt.WithAngleBracketExpressions())
49+
.Build();
50+
51+
// Or use dollar-sign delimiters
52+
var engine = new TemplateEngineBuilder()
53+
.UseStringTemplate(opt => opt.WithDollarDelimitedExpressions())
54+
.Build();
55+
```
56+
57+
By default, StringTemplate uses `<...>` delimiters.
58+
59+
## Global Configuration
60+
61+
To further customize engine behavior, such as enabling HTML encoding, you can attach a global configuration using .`WithConfiguration(...)`. This configuration is shared across all engines and controls cross-cutting concerns — for example, whether the rendered output should be HTML-encoded.
62+
63+
This is different from engine-specific options, which are only applicable to one particular engine (like delimiter styles for StringTemplate).
64+
65+
```csharp
66+
var engine = new TemplateEngineBuilder()
67+
.UseScriban()
68+
.WithConfiguration(config => config.WithHtmlEncode())
69+
.Build();
70+
```
71+
72+
Available options:
73+
74+
| Method | Description |
75+
|----------------|-------------------------------|
76+
| `WithHtmlEncode()` | Enables HTML encoding in output |
77+
| `WithoutHtmlEncode()` | Disables HTML encoding (default) |
78+
79+
The resulting `ITemplateEngine` exposes this configuration via the `Configuration` property.
80+
81+
## Summary
82+
83+
- Use `TemplateEngineBuilder` to select and configure your desired template engine
84+
- Most engines require no options
85+
- `StringTemplate` supports delimiter configuration via an optional builder
86+
- Global behavior (like HTML encoding) can be configured via WithConfiguration(...)
87+
- Once built, the result is an `ITemplateEngine` instance ready for rendering

src/Didot.Core/ITemplateEngine.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace Didot.Core;
88
public interface ITemplateEngine
99
{
10+
public TemplateConfiguration Configuration { get; }
1011
void AddMappings(string mapKey, IDictionary<string, object> mappings);
1112
void AddFormatter(string name, Func<object?, string> function);
1213
void AddFunction(string name, Func<string> template);

src/Didot.Core/TemplateConfiguration.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using System.Threading.Tasks;
77

88
namespace Didot.Core;
9-
public record TemplateConfiguration
9+
public record struct TemplateConfiguration
1010
(
1111
bool HtmlEncode = false
1212
)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Didot.Core.TemplateEngines;
7+
using HandlebarsDotNet;
8+
9+
namespace Didot.Core;
10+
11+
/// <summary>
12+
/// Builder class for creating template engine instances with a fluent API.
13+
/// </summary>
14+
public class TemplateEngineBuilder : ITemplateEngineConfigurabable
15+
{
16+
private Type? _templateEngineType;
17+
private ITemplateEngineOptionsBuilder? _optionsBuilder;
18+
private TemplateConfigurationBuilder? _configBuilder;
19+
20+
public ITemplateEngineConfigurabable UseDotLiquid()
21+
{
22+
_templateEngineType = typeof(DotLiquidWrapper);
23+
return this;
24+
}
25+
26+
public ITemplateEngineConfigurabable UseFluid()
27+
{
28+
_templateEngineType = typeof(FluidWrapper);
29+
return this;
30+
}
31+
32+
public ITemplateEngineConfigurabable UseHandlebars()
33+
{
34+
_templateEngineType = typeof(HandlebarsWrapper);
35+
return this;
36+
}
37+
38+
public ITemplateEngineConfigurabable UseMorestachio()
39+
{
40+
_templateEngineType = typeof(MorestachioWrapper);
41+
return this;
42+
}
43+
44+
public ITemplateEngineConfigurabable UseScriban()
45+
{
46+
_templateEngineType = typeof(ScribanWrapper);
47+
return this;
48+
}
49+
50+
public ITemplateEngineConfigurabable UseSmartFormat()
51+
{
52+
_templateEngineType = typeof(SmartFormatWrapper);
53+
return this;
54+
}
55+
56+
public ITemplateEngineConfigurabable UseStringTemplate()
57+
{
58+
_templateEngineType = typeof(StringTemplateWrapper);
59+
return this;
60+
}
61+
62+
public ITemplateEngineConfigurabable UseStringTemplate(Func<StringTemplateOptionsBuilder, StringTemplateOptionsBuilder> builder)
63+
{
64+
_templateEngineType = typeof(StringTemplateWrapper);
65+
_optionsBuilder = builder(new());
66+
return this;
67+
}
68+
69+
ITemplateEngineBuildable ITemplateEngineConfigurabable.WithConfiguration(Func<TemplateConfigurationBuilder, TemplateConfigurationBuilder> builder)
70+
{
71+
_configBuilder = builder(new());
72+
return this;
73+
}
74+
75+
ITemplateEngine ITemplateEngineBuildable.Build()
76+
{
77+
if (_templateEngineType is null)
78+
throw new InvalidOperationException("Template engine type is not set. Call one of the Use* methods first (e.g., UseHandlebars(), UseStringTemplate(), etc.).");
79+
80+
var parameters = new List<object>();
81+
if (_optionsBuilder is not null)
82+
parameters.Add(_optionsBuilder.Build());
83+
84+
if (_configBuilder is not null)
85+
parameters.Add(_configBuilder.Build());
86+
87+
if (parameters.Count == 0)
88+
try
89+
{
90+
return (ITemplateEngine)Activator.CreateInstance(_templateEngineType)!;
91+
}
92+
catch (Exception ex) when (ex is not InvalidOperationException)
93+
{
94+
throw new InvalidOperationException($"Failed to create instance of {_templateEngineType.Name}.", ex);
95+
}
96+
else
97+
{
98+
var constructor = _templateEngineType.GetConstructor([.. parameters.Select(p => p.GetType())]);
99+
return constructor is null
100+
? throw new InvalidOperationException($"No suitable constructor found for type {_templateEngineType.Name}.")
101+
: (ITemplateEngine)constructor.Invoke([.. parameters]);
102+
}
103+
}
104+
}
105+
106+
public interface ITemplateEngineBuildable
107+
{
108+
ITemplateEngine Build();
109+
}
110+
111+
public interface ITemplateEngineConfigurabable : ITemplateEngineBuildable
112+
{
113+
ITemplateEngineBuildable WithConfiguration(Func<TemplateConfigurationBuilder, TemplateConfigurationBuilder> builder);
114+
}

src/Didot.Core/TemplateEngines/BaseTemplateWrapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
namespace Didot.Core.TemplateEngines;
88
public abstract class BaseTemplateEngine : ITemplateEngine
99
{
10-
protected TemplateConfiguration Configuration { get; }
10+
public TemplateConfiguration Configuration { get; }
1111

1212
public BaseTemplateEngine()
1313
: this(new TemplateConfiguration())
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Didot.Core.TemplateEngines;
8+
public interface ITemplateEngineOptions
9+
{ }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Didot.Core.TemplateEngines;
8+
public interface ITemplateEngineOptionsBuilder
9+
{
10+
ITemplateEngineOptions Build();
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Didot.Core.TemplateEngines;
8+
public record struct StringTemplateOptions(
9+
StringTemplateOptions.CharCouple Delimiters
10+
) : ITemplateEngineOptions
11+
{
12+
public record struct CharCouple(char Left, char Right) { }
13+
14+
private static readonly StringTemplateOptions _default = new StringTemplateOptions(new CharCouple('<', '>'));
15+
public static readonly StringTemplateOptions Default = _default;
16+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using PocketCsvReader.Configuration;
7+
8+
namespace Didot.Core.TemplateEngines;
9+
public class StringTemplateOptionsBuilder : ITemplateEngineOptionsBuilder
10+
{
11+
private enum Delimiter
12+
{
13+
Dollar,
14+
AngleBracket
15+
}
16+
17+
private Delimiter _delimiter = Delimiter.AngleBracket;
18+
19+
public StringTemplateOptionsBuilder WithDollarDelimitedExpressions()
20+
{
21+
_delimiter = Delimiter.Dollar;
22+
return this;
23+
}
24+
25+
public StringTemplateOptionsBuilder WithAngleBracketExpressions()
26+
{
27+
_delimiter = Delimiter.AngleBracket;
28+
return this;
29+
}
30+
31+
public ITemplateEngineOptions Build()
32+
=> _delimiter switch
33+
{
34+
Delimiter.Dollar => new StringTemplateOptions(new StringTemplateOptions.CharCouple('$', '$')),
35+
Delimiter.AngleBracket => new StringTemplateOptions(new StringTemplateOptions.CharCouple('<', '>')),
36+
_ => throw new InvalidOperationException("No delimiter style was specified.Call WithDollarDelimitedExpressions() or WithAngleBracketExpressions() before building.")
37+
};
38+
}

0 commit comments

Comments
 (0)