Skip to content

Commit 1091e95

Browse files
authored
Merge pull request #28 from tgiachi/feature/templating-include
feat(templating): support {{ include }} in Scriban templates
2 parents 1db4d90 + 98becdb commit 1091e95

3 files changed

Lines changed: 122 additions & 2 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using Scriban;
2+
using Scriban.Parsing;
3+
using Scriban.Runtime;
4+
5+
namespace SquidStd.Templating.Internal;
6+
7+
/// <summary>
8+
/// Scriban <see cref="ITemplateLoader" /> that resolves <c>{{ include }}</c> names from the renderer's
9+
/// registered template sources.
10+
/// </summary>
11+
internal sealed class NamedTemplateLoader : ITemplateLoader
12+
{
13+
private readonly IReadOnlyDictionary<string, string> _sources;
14+
15+
public NamedTemplateLoader(IReadOnlyDictionary<string, string> sources)
16+
{
17+
_sources = sources;
18+
}
19+
20+
public string GetPath(TemplateContext context, SourceSpan callerSpan, string templateName)
21+
=> templateName;
22+
23+
public string Load(TemplateContext context, SourceSpan callerSpan, string templatePath)
24+
{
25+
if (!_sources.TryGetValue(templatePath, out var source))
26+
{
27+
throw new TemplateException($"Template '{templatePath}' is not registered (include).");
28+
}
29+
30+
return source;
31+
}
32+
33+
public ValueTask<string> LoadAsync(TemplateContext context, SourceSpan callerSpan, string templatePath)
34+
=> new(Load(context, callerSpan, templatePath));
35+
}

src/SquidStd.Templating/Services/ScribanTemplateRenderer.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using System.Collections.Concurrent;
22
using Scriban;
3+
using Scriban.Runtime;
34
using Scriban.Syntax;
45
using SquidStd.Abstractions.Interfaces.Services;
56
using SquidStd.Core.Directories;
67
using SquidStd.Templating.Interfaces;
8+
using SquidStd.Templating.Internal;
79

810
namespace SquidStd.Templating.Services;
911

@@ -17,10 +19,13 @@ public sealed class ScribanTemplateRenderer : ITemplateRenderer, ISquidStdServic
1719

1820
private readonly DirectoriesConfig _directories;
1921
private readonly ConcurrentDictionary<string, Template> _named = new(StringComparer.Ordinal);
22+
private readonly ConcurrentDictionary<string, string> _sources = new(StringComparer.Ordinal);
23+
private readonly NamedTemplateLoader _loader;
2024

2125
public ScribanTemplateRenderer(DirectoriesConfig directories)
2226
{
2327
_directories = directories;
28+
_loader = new NamedTemplateLoader(_sources);
2429
}
2530

2631
/// <inheritdoc />
@@ -54,6 +59,7 @@ public void Register(string name, string template)
5459
ArgumentException.ThrowIfNullOrWhiteSpace(template);
5560

5661
_named[name] = Parse(template, name);
62+
_sources[name] = template;
5763
}
5864

5965
/// <inheritdoc />
@@ -93,11 +99,21 @@ private static Template Parse(string text, string name)
9399
return template;
94100
}
95101

96-
private static async ValueTask<string> RenderCompiledAsync(Template template, object? model, string name)
102+
private async ValueTask<string> RenderCompiledAsync(Template template, object? model, string name)
97103
{
104+
var context = new TemplateContext { TemplateLoader = _loader };
105+
var scriptObject = new ScriptObject();
106+
107+
if (model is not null)
108+
{
109+
scriptObject.Import(model);
110+
}
111+
112+
context.PushGlobal(scriptObject);
113+
98114
try
99115
{
100-
return await template.RenderAsync(model);
116+
return await template.RenderAsync(context);
101117
}
102118
catch (ScriptRuntimeException ex)
103119
{

tests/SquidStd.Tests/Templating/ScribanTemplateRendererTests.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,75 @@ public async Task StartAsync_AutoLoadsTemplatesFromDirectory()
8181
Assert.Equal("Welcome squid", result);
8282
}
8383

84+
[Fact]
85+
public async Task Include_ResolvesRegisteredTemplate()
86+
{
87+
using var temp = new TempDirectory();
88+
var renderer = NewRenderer(temp.Path);
89+
renderer.Register("partials/header", "HEADER");
90+
renderer.Register("page", "{{ include 'partials/header' }} BODY");
91+
92+
var result = await renderer.RenderByNameAsync("page", null);
93+
94+
Assert.Equal("HEADER BODY", result);
95+
}
96+
97+
[Fact]
98+
public async Task Include_FromInlineTemplate_Works()
99+
{
100+
using var temp = new TempDirectory();
101+
var renderer = NewRenderer(temp.Path);
102+
renderer.Register("partials/header", "HEADER");
103+
104+
var result = await renderer.RenderAsync("{{ include 'partials/header' }}!", null);
105+
106+
Assert.Equal("HEADER!", result);
107+
}
108+
109+
[Fact]
110+
public async Task Include_WithModel_BindsModel()
111+
{
112+
using var temp = new TempDirectory();
113+
var renderer = NewRenderer(temp.Path);
114+
renderer.Register("partials/hello", "Hi {{ user.name }}");
115+
renderer.Register("page", "{{ include 'partials/hello' }}");
116+
117+
var result = await renderer.RenderByNameAsync("page", new { User = new { Name = "squid" } });
118+
119+
Assert.Equal("Hi squid", result);
120+
}
121+
122+
[Fact]
123+
public async Task Include_MissingTemplate_Throws()
124+
{
125+
using var temp = new TempDirectory();
126+
var renderer = NewRenderer(temp.Path);
127+
128+
await Assert.ThrowsAsync<TemplateException>(
129+
async () => await renderer.RenderAsync("{{ include 'nope' }}", null)
130+
);
131+
}
132+
133+
[Fact]
134+
public async Task Include_AutoLoadedFromDisk_Works()
135+
{
136+
using var temp = new TempDirectory();
137+
var templatesDir = Path.Combine(temp.Path, "templates");
138+
Directory.CreateDirectory(Path.Combine(templatesDir, "partials"));
139+
await File.WriteAllTextAsync(Path.Combine(templatesDir, "partials", "header.tmpl"), "HEADER");
140+
await File.WriteAllTextAsync(
141+
Path.Combine(templatesDir, "page.tmpl"),
142+
"{{ include 'partials/header' }} BODY"
143+
);
144+
145+
var renderer = NewRenderer(temp.Path);
146+
await renderer.StartAsync();
147+
148+
var result = await renderer.RenderByNameAsync("page", null);
149+
150+
Assert.Equal("HEADER BODY", result);
151+
}
152+
84153
private static ScribanTemplateRenderer NewRenderer(string root)
85154
=> new(new(root, []));
86155
}

0 commit comments

Comments
 (0)