Skip to content
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<!-- Solution version numbers -->
<PropertyGroup>
<MajorVersion>3</MajorVersion>
<MinorVersion>1</MinorVersion>
<MinorVersion>2</MinorVersion>
<PatchVersion>0</PatchVersion>
</PropertyGroup>
<!-- Disable automatic package publishing -->
Expand Down
6 changes: 6 additions & 0 deletions Hyperbee.Templating.sln
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Templating.Benchma
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "docs", "docs\docs.shproj", "{8409DDE0-C540-4A94-BF7F-9403888BEDAC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hyperbee.Templating.Provider.XS", "src\Hyperbee.Templating.Provider.XS\Hyperbee.Templating.Provider.XS.csproj", "{EA264696-D88A-288E-0D8E-C834780D5F9E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -55,6 +57,10 @@ Global
{EB7D2A85-3C82-444A-84CD-D245DCF951CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB7D2A85-3C82-444A-84CD-D245DCF951CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB7D2A85-3C82-444A-84CD-D245DCF951CE}.Release|Any CPU.Build.0 = Release|Any CPU
{EA264696-D88A-288E-0D8E-C834780D5F9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA264696-D88A-288E-0D8E-C834780D5F9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA264696-D88A-288E-0D8E-C834780D5F9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA264696-D88A-288E-0D8E-C834780D5F9E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Hyperbee.Templating.Compiler;
using Hyperbee.Templating.Text;
using Hyperbee.XS;
using Hyperbee.XS.Core;
using static System.Linq.Expressions.Expression;

namespace Hyperbee.Templating.Provider.XS.Compiler;

public delegate TokenExpression CompileLambda( Expression<TokenExpression> lambda );

public sealed class XsTokenExpressionProvider : ITokenExpressionProvider
{
private readonly ConcurrentDictionary<string, TokenExpression> TokenExpressions = new();
private readonly CompileLambda _compile;
private readonly XsParser _xsParser;

public XsTokenExpressionProvider(
CompileLambda compile = null,
TypeResolver typeResolver = null,
List<IParseExtension> extensions = null )
{
_compile = compile ?? (lambda => lambda.Compile());
typeResolver ??= new MemberTypeResolver( ReferenceManager.Create() );

_xsParser = new XsParser(
new XsConfig( typeResolver ) { Extensions = extensions ?? [] }
);
}

[MethodImpl( MethodImplOptions.AggressiveInlining )]
public TokenExpression GetTokenExpression( string codeExpression, MemberDictionary members )
{
return TokenExpressions.GetOrAdd( codeExpression, _ => Compile( codeExpression ) );
}

[MethodImpl( MethodImplOptions.AggressiveInlining )]
public void Reset()
{
TokenExpressions.Clear();
}

private TokenExpression Compile( ReadOnlySpan<char> codeExpression )
{
var start = codeExpression.IndexOf( "=>" );
var argument = codeExpression[..start].Trim().ToString();
var body = codeExpression[(start + 2)..].Trim().ToString();

var scope = new ParseScope();

try
{
scope.EnterScope( FrameType.Method );

var codeParameter = Parameter( typeof( IReadOnlyMemberDictionary ), argument );

scope.Variables.Add( argument, codeParameter );

var expression = _xsParser.Parse( body, scope: scope );
var expressionBody = expression as BlockExpression;

var lambdaParameter = Parameter( typeof( IReadOnlyMemberDictionary ) );

// create a new block expression assigning the parameter to the argument
var expressions = new List<Expression> { Assign( codeParameter, lambdaParameter ) };
if ( expressionBody == null )
expressions.Add( expression );
else
expressions.AddRange( expressionBody.Expressions );

var lambda = Lambda<TokenExpression>(
Convert(
Block(
expressionBody?.Variables,
expressions
),
typeof( object )
),
lambdaParameter );

return _compile( lambda );
}
finally
{
scope.ExitScope();
}
}

public class MemberTypeResolver : TypeResolver
{
private static readonly MethodInfo MemberInvoke = typeof( IReadOnlyMemberDictionary )
.GetMethod( nameof( IReadOnlyMemberDictionary.Invoke ), [typeof( string ), typeof( object[] )] )!;

private static readonly MethodInfo MemberGetValueAs = typeof( IReadOnlyMemberDictionary )
.GetMethod( nameof( IReadOnlyMemberDictionary.GetValueAs ), [typeof( string )] )!;

private static readonly PropertyInfo MemberIndexer = typeof( MemberDictionary )
.GetProperties()
.First( x => x.GetIndexParameters().Length > 0 );
public MemberTypeResolver( ReferenceManager referenceManager ) : base( referenceManager ) { }

// Resolves a member expression for the given target expression.
//
// 1. x => x.someProp to x["someProp"]
// 2. x => x.someProp<T> to x.GetValueAs<T>("someProp")
// 3. x => x.someMethod(..) to x.Invoke("someMethod", ..)

public override Expression RewriteMemberExpression( Expression targetExpression, string name, IReadOnlyList<Type> typeArgs, IReadOnlyList<Expression> args )
{
if ( targetExpression.Type != typeof( IReadOnlyMemberDictionary ) )
return base.RewriteMemberExpression( targetExpression, name, typeArgs, args );

if ( args != null )
{
return Call(
targetExpression,
MemberInvoke,
Constant( name ),
NewArrayInit( typeof( object ), args )
);
}

if ( typeArgs != null )
{
return Call(
targetExpression,
MemberGetValueAs
.MakeGenericMethod( typeArgs[0] ),
Constant( name ) );
}

return Property(
Convert( targetExpression, typeof( MemberDictionary ) ),
MemberIndexer,
Constant( name ) );

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>true</IsPackable>
<Authors>Stillpoint Software, Inc.</Authors>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>templating;token;xs</PackageTags>
<PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://stillpoint-software.github.io/hyperbee.templating/</PackageProjectUrl>
<TargetFrameworks>net9.0</TargetFrameworks>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<Copyright>Stillpoint Software, Inc.</Copyright>
<Title>Hyperbee Templating Provider XS</Title>
<Description>
Adds an ITokenExpressionProvider powder by Hyperbee.XS
</Description>
<RepositoryUrl>https://github.com/Stillpoint-Software/Hyperbee.Templating</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageReleaseNotes>https://github.com/Stillpoint-Software/hyperbee.templating/releases/latest</PackageReleaseNotes>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(AssemblyName).Benchmark</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<None Update="$(MSBuildProjectName).csproj.DotSettings" Visible="false" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\assets\icon.png" Pack="true" Visible="false" PackagePath="/" />
<None Include="..\..\README.md" Pack="true" Visible="true" PackagePath="/" Link="README.md" />
<None Include="..\..\LICENSE" Pack="true" Visible="false" PackagePath="/" />
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FastExpressionCompiler" Version="5.1.1" />
<PackageReference Include="Hyperbee.Resources" Version="2.0.2" />
<PackageReference Include="Hyperbee.XS" Version="1.3.3" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.13.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Hyperbee.Templating\Hyperbee.Templating.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -11,9 +11,11 @@

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="Hyperbee.XS" Version="1.3.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Hyperbee.Templating.Provider.XS\Hyperbee.Templating.Provider.XS.csproj" />
<ProjectReference Include="..\..\src\Hyperbee.Templating\Hyperbee.Templating.csproj" />
<ProjectReference Include="..\Hyperbee.Templating.Tests\Hyperbee.Templating.Tests.csproj" />
</ItemGroup>
Expand Down
34 changes: 34 additions & 0 deletions test/Hyperbee.Templating.Benchmark/TemplateBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
using BenchmarkDotNet.Attributes;
using FastExpressionCompiler;
using Hyperbee.Templating.Configure;
using Hyperbee.Templating.Provider.XS.Compiler;
using Hyperbee.Templating.Text;
using Hyperbee.XS.Core;

namespace Hyperbee.Templating.Benchmark;

public class TemplateBenchmarks
{
private static readonly TypeResolver TypeResolver = new XsTokenExpressionProvider.MemberTypeResolver( ReferenceManager.Create() );

[Benchmark( Baseline = true )]
public void ParserSingleLine()
{
Expand Down Expand Up @@ -68,5 +74,33 @@ public void InlineBlockExpression()
}
} );
}

[Benchmark]
public void InlineBlockExpressionXs()
{
const string expression = "{{name}}";
const string definition =
"""
{{name:{{x => {
switch( x.choice )
{
case "1": "me";
case "2": "you";
default: "default";
};
} }} }}
""";

const string template = $"{definition}hello {expression}.";

Template.Render( template, new TemplateOptions
{
Variables = { ["choice"] = "2" },
TokenExpressionProvider = new XsTokenExpressionProvider(
compile: lambda => lambda.CompileFast(),
typeResolver: TypeResolver
)
} );
}
}

Loading
Loading