Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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