Skip to content

Commit

Permalink
EXPERIMENTAL: Add Handlebars Support (#32)
Browse files Browse the repository at this point in the history
Prototyping and exploration of what Handlebars would look likein.

Handlebars.Net as the supported engine
Handlebar content can be rendered using files with `.hbs` extension in VSCode extension.
Enables nested JSON types in the attributes to support more complex attributes
  • Loading branch information
lbuesching authored Nov 26, 2024
1 parent ed70dea commit a20f550
Show file tree
Hide file tree
Showing 28 changed files with 300 additions and 34 deletions.
3 changes: 3 additions & 0 deletions src/Sage.Engine.Tests/CompilerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ public void TestAssemblyGeneration(string sourceFile)
}

[Test]
[TestCase("%%=ADD(1,2)=%%", ContentType.Handlebars, "%%=ADD(1,2)=%%")]
[TestCase("%%=ADD(1,2)=%%", ContentType.AMPscript, "3")]
[TestCase("{{#if true}}Hello{{/if}}", ContentType.AMPscript, "")]
[TestCase("{{#if true}}Hello{{/if}}", ContentType.Handlebars, "Hello")]
public void TestRenderer(string input, ContentType type, string expected)
{
var content = new EmbeddedContent(input, "TEST", "TEST", 1, type);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"people": [
{
"name": "Adam"
},
{
"name": "Howard"
}
]
}
6 changes: 6 additions & 0 deletions src/Sage.Engine.Tests/Corpus/Handlebars/eachHelper.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
===========
eachHelper
===========
{{#each people}}<h1>{{name}}</h1>{{/each}}
++++++++++
<h1>Adam</h1><h1>Howard</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"isActiveFalse": false,
"isActiveTrue": true
}
12 changes: 12 additions & 0 deletions src/Sage.Engine.Tests/Corpus/Handlebars/ifHelper.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
===========
If True Block
===========
{{#if isActiveTrue}}hello{{else}}world{{/if}}
++++++++++
hello
===========
If False Block
===========
{{#if isActiveFalse}}hello{{else}}world{{/if}}
++++++++++
world
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"titleTags": "First Template <p> Tags",
"titleNoTags": "First Template No Tags"
}
12 changes: 12 additions & 0 deletions src/Sage.Engine.Tests/Corpus/Handlebars/stringEncoding.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
===========
Escaping
===========
{{titleTags}}
++++++++++
First Template &lt;p&gt; Tags
===========
No Escaping
===========
{{titleNoTags}}
++++++++++
First Template No Tags
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"isActiveTrue": true,
"isActiveFalse": false
}
11 changes: 11 additions & 0 deletions src/Sage.Engine.Tests/Corpus/Handlebars/unlessHelper.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
===========
Unless Helper False
===========
{{#unless isActiveFalse}}hello{{/unless}}
++++++++++
hello
===========
Unless Helper True
===========
{{#unless isActiveTrue}}hello{{/unless}}
++++++++++
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"primary": {
"name": "Adam"
},
"name": "Howard"
}
6 changes: 6 additions & 0 deletions src/Sage.Engine.Tests/Corpus/Handlebars/withHelper.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
===========
WithHelper Tests
===========
{{#with primary}}{{name}}{{/with}}
++++++++++
Adam
32 changes: 32 additions & 0 deletions src/Sage.Engine.Tests/HandlebarTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2022, salesforce.com, inc.
// All rights reserved.
// SPDX-License-Identifier: Apache-2.0
// For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/Apache-2.0

namespace Sage.Engine.Tests
{
using NUnit.Framework;

/// <summary>
/// Basic tests for the language features and runtime
/// </summary>
public class HandlebarTests : SageTest
{
[Test]
[RuntimeTest("Handlebars")]
public EngineTestResult TestHandlebars(CorpusData test)
{
try
{
var result = TestUtils.GetOutputFromHandlebarTest(_serviceProvider, test);
Assert.That(result.Output, Is.EqualTo(test.Output));
return result;
}
catch (CompileCodeException e)
{
Assert.Fail(e.Message);
return new EngineTestResult("!");
}
}
}
}
30 changes: 30 additions & 0 deletions src/Sage.Engine.Tests/Sage.Engine.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,36 @@
<None Update="Corpus\Function\utility.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Corpus\Handlebars\unlessHelper.subscribercontext.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Corpus\Handlebars\stringEncoding.subscribercontext.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Corpus\Handlebars\ifHelper.subscribercontext.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Corpus\Handlebars\eachHelper.subscribercontext.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Corpus\Handlebars\eachHelper.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Corpus\Handlebars\stringEncoding.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Corpus\Handlebars\unlessHelper.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Corpus\Handlebars\withHelper.subscribercontext.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Corpus\Handlebars\withHelper.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Corpus\Handlebars\ifHelper.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Corpus\Language\attributes.subscribercontext.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
26 changes: 23 additions & 3 deletions src/Sage.Engine.Tests/TestUtils.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// Copyright (c) 2022, salesforce.com, inc.
// Copyright (c) 2022, salesforce.com, inc.
// All rights reserved.
// SPDX-License-Identifier: Apache-2.0
// For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/Apache-2.0

using Antlr4.Runtime.Tree;
using Microsoft.Extensions.DependencyInjection;
using Sage.Engine.Compiler;
using Sage.Engine.Data;
using Sage.Engine.Parser;
using Sage.Engine.Runtime;

Expand Down Expand Up @@ -42,6 +41,27 @@ private static RuntimeContext GetTestRuntimeContext(IServiceProvider serviceProv
}


/// <summary>
/// Uses Handlebars to compile the content instead of the AMPscript compiler
/// </summary>
public static EngineTestResult GetOutputFromHandlebarTest(IServiceProvider serviceProvider, CorpusData test)
{
CompilationOptions options = new CompilerOptionsBuilder()
.WithContent(new EmbeddedContent(test.Code, test.FileFriendlyName, test.FileFriendlyName, 1, ContentType.Handlebars))
.Build();

try
{
string result = serviceProvider.GetService<ICompiler>()
!.Compile(options, GetTestRuntimeContext(serviceProvider, options, test), test.SubscriberContext);
return new EngineTestResult(result.ReplaceLineEndings("\n").Trim());
}
catch (Exception)
{
return new EngineTestResult("!");
}
}

/// <summary>
/// Executes the engine and gets the expected result from the engine
/// </summary>
Expand Down Expand Up @@ -71,4 +91,4 @@ public static EngineTestResult GetOutputFromTest(IServiceProvider serviceProvide
return new EngineTestResult("!");
}
}
}
}
7 changes: 7 additions & 0 deletions src/Sage.Engine/Content/ContentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ public static void AddLocalDiskContentClient(this IServiceCollection services, A
/// </summary>
public static ContentType InferContentTypeFromFilename(string path)
{
string extension = Path.GetExtension(path).ToLowerInvariant();

if (extension == ".hbs")
{
return ContentType.Handlebars;
}

return ContentType.AMPscript;
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Sage.Engine/Content/IContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ namespace Sage.Engine.Compiler
/// </summary>
public enum ContentType
{
AMPscript
AMPscript,
Handlebars
};

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Sage.Engine/Content/LocalDiskContentClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ public LocalDiskContentClient(
return new LocalFileContent(id, filePath, 1, ContentType.AMPscript);
}

filePath = Path.Combine(_options.InputDirectory.FullName, $"{id}.hbs");
if (File.Exists(filePath))
{
return new LocalFileContent(id, filePath, 1, ContentType.Handlebars);
}

return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Sage.Engine.Data.DependencyInjection;
using Sage.Engine.Data.Sqlite;
using Sage.Engine.Extensions;
using Sage.Engine.Handlebars;

namespace Sage.Engine.DependencyInjection
{
Expand Down Expand Up @@ -61,6 +62,7 @@ public static void AddSage(
services.AddLocalDiskContentClient();
services.AddScoped<Renderer>();
services.AddScoped<ICompiler, AmpscriptCompiler>();
services.AddScoped<ICompiler, HandlebarsCompiler>();
}
}
}
44 changes: 44 additions & 0 deletions src/Sage.Engine/Handlebars/HandlebarsCompiler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2023, salesforce.com, inc.
// All rights reserved.
// SPDX-License-Identifier: Apache-2.0
// For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/Apache-2.0

using HandlebarsDotNet;
using HandlebarsDotNet.Extension.Json;
using Sage.Engine.Compiler;
using Sage.Engine.Runtime;

namespace Sage.Engine.Handlebars
{
/// <summary>
/// Uses HandlebarsDotNet to compile content to a string.
///
/// Does not support AMPscript embedded in the content.
/// </summary>
internal class HandlebarsCompiler : IHandlebarsCompiler
{
private IHandlebars _handlebar;

public HandlebarsCompiler()
{
_handlebar = HandlebarsDotNet.Handlebars.Create();
_handlebar.Configuration.UseJson();
}

public bool CanCompile(IContent content)
{
return content.ContentType == ContentType.Handlebars;
}

public string Compile(CompilationOptions options, RuntimeContext runtimeContext, SubscriberContext? subscriberContext)
{
var template = _handlebar.Compile(options.Content.GetTextReader());

StringWriter writer = new StringWriter();

template(writer, subscriberContext?.GetRichAttributes());

return writer.ToString();
}
}
}
14 changes: 14 additions & 0 deletions src/Sage.Engine/Handlebars/IHandlebarsCompiler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2023, salesforce.com, inc.
// All rights reserved.
// SPDX-License-Identifier: Apache-2.0
// For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/Apache-2.0

namespace Sage.Engine.Handlebars
{
/// <summary>
/// Renders a piece of Handlebars content to a string.
/// </summary>
public interface IHandlebarsCompiler : ICompiler
{
}
}
20 changes: 4 additions & 16 deletions src/Sage.Engine/Runtime/RuntimeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,8 @@ public RuntimeContext(
_contentBuilderContentClient = provider.GetRequiredService<IContentBuilderContentClient>();
_dataExtensionClient = provider.GetRequiredService<IDataExtensionClient>();
_dataExtensionClient.ConnectAsync().Wait();

string subscriberContextFile =
Path.Combine(Path.GetDirectoryName(_rootCompilationOptions.Content.Location), "subscriber.json");
if (subscriberContext != null)
{
_subscriberContext = subscriberContext;
}
else if (Path.Exists(subscriberContextFile))
{
_subscriberContext = new SubscriberContext(File.ReadAllText(subscriberContextFile));
}
else
{
_subscriberContext = new SubscriberContext(null);
}

_subscriberContext = subscriberContext ?? new SubscriberContext(null);

_stackFrame.Push(new StackFrame(_rootCompilationOptions.GeneratedMethodName, _rootCompilationOptions.Content));
}
Expand Down Expand Up @@ -229,14 +216,15 @@ public SubscriberContext GetSubscriberContext()

internal string? CompileAndExecuteReferencedCode(CompilationOptions currentOptions)
{
CompileResult compileResult = CSharpCompiler.GenerateAssemblyFromSource(currentOptions);

string poppedContext;

PushContext(currentOptions.GeneratedMethodName, currentOptions.Content);

try
{

CompileResult compileResult = CSharpCompiler.GenerateAssemblyFromSource(currentOptions);
compileResult.Execute(this);
}
finally
Expand Down
Loading

0 comments on commit a20f550

Please sign in to comment.