Skip to content

[FEATURE]: Add foreach looping #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 66 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
4841f04
Create draft PR for #13
github-actions[bot] Jul 25, 2024
edc0a05
Got the buffer and inMemory WIP with foreach loop
varndellwagglebee Aug 6, 2024
d844904
wip fixing tests
varndellwagglebee Aug 7, 2024
d3e6977
wip tests
varndellwagglebee Aug 7, 2024
5f25cd4
WIP with tests
varndellwagglebee Aug 8, 2024
7752cf0
wip Tests
varndellwagglebee Aug 8, 2024
2f7283d
WIP Tests Are working
varndellwagglebee Aug 8, 2024
e51d9ea
Fixed all the tests
varndellwagglebee Aug 9, 2024
88177fc
WIP Add 'while' and refactor to use TokenProcessor
bfarmer67 Aug 11, 2024
0b5c3cb
lazy is better
bfarmer67 Aug 11, 2024
5290f6a
create a buffermanager and template incremental parser to use it
bfarmer67 Aug 11, 2024
0b1092d
Create draft PR for #15
github-actions[bot] Aug 11, 2024
0bb7366
Merge branch 'bf-playground' into feature/15-feature-improve-template…
bfarmer67 Aug 11, 2024
860dda4
Updated code formatting to match rules in .editorconfig
invalid-email-address Aug 11, 2024
f2e353d
corrected buffermanager corner cases
bfarmer67 Aug 12, 2024
cda5352
Updated code formatting to match rules in .editorconfig
invalid-email-address Aug 12, 2024
98917f4
Remove in-memory parser implementation
bfarmer67 Aug 12, 2024
d2b76a3
Updated code formatting to match rules in .editorconfig
invalid-email-address Aug 12, 2024
8150dc7
cleanup
bfarmer67 Aug 12, 2024
c6e1807
Updated code formatting to match rules in .editorconfig
invalid-email-address Aug 12, 2024
96fe0e7
Don't package benchmark
bfarmer67 Aug 13, 2024
81482aa
Adjusted readme and project structure to follow patterns in our other…
bfarmer67 Aug 13, 2024
906961d
WIP adding each
varndellwagglebee Aug 13, 2024
6463815
additiona
varndellwagglebee Aug 13, 2024
90c8c11
WIp
varndellwagglebee Aug 13, 2024
efc282f
fixed merge conflicts
varndellwagglebee Aug 28, 2024
4a66af3
merge conflicts
varndellwagglebee Aug 28, 2024
e8a7fd1
Updated code formatting to match rules in .editorconfig
invalid-email-address Aug 28, 2024
4193ff2
Added each process
varndellwagglebee Aug 29, 2024
39585ca
Merge branch 'feature/13-feature-add-foreach-looping' of https://gith…
varndellwagglebee Aug 29, 2024
a459170
Updated code formatting to match rules in .editorconfig
invalid-email-address Aug 29, 2024
9b698bf
Wip foreach
varndellwagglebee Oct 30, 2024
245e3d2
updated processor
varndellwagglebee Oct 30, 2024
5554bd0
Second try.
varndellwagglebee Nov 4, 2024
041ae5b
WIP Each changes
varndellwagglebee Nov 8, 2024
19c029b
Had to update the truthyness when going through the enumerator.
varndellwagglebee Nov 11, 2024
0c896dd
Preliminary completion of Each
varndellwagglebee Nov 12, 2024
1bbbbe0
Changes to enumerator handling
bfarmer67 Nov 14, 2024
968ab55
Updated code formatting to match rules in .editorconfig
invalid-email-address Nov 14, 2024
184a456
A little cleanup.
varndellwagglebee Nov 14, 2024
4d14572
Merge branch 'develop' into feature/13-feature-add-foreach-looping
varndellwagglebee Nov 14, 2024
5b3c0ed
Clean up token processor
bfarmer67 Nov 14, 2024
c2bd9fa
Updated code formatting to match rules in .editorconfig
invalid-email-address Nov 14, 2024
f95214e
remove useless comments
bfarmer67 Nov 14, 2024
eab0e0c
Merge remote-tracking branch 'origin/feature/13-feature-add-foreach-l…
bfarmer67 Nov 14, 2024
ccdca87
clean up
bfarmer67 Nov 14, 2024
484ac90
Updated code formatting to match rules in .editorconfig
invalid-email-address Nov 14, 2024
b9ae425
clean up
bfarmer67 Nov 14, 2024
b839461
more clean up
bfarmer67 Nov 14, 2024
73e3639
more clean up
bfarmer67 Nov 15, 2024
de6fb5a
Updated code formatting to match rules in .editorconfig
invalid-email-address Nov 15, 2024
c181141
comments
bfarmer67 Nov 15, 2024
c103554
Merge remote-tracking branch 'origin/feature/13-feature-add-foreach-l…
bfarmer67 Nov 15, 2024
c533777
Updated code formatting to match rules in .editorconfig
invalid-email-address Nov 15, 2024
5b8c1ae
WIP another for each.
varndellwagglebee Dec 16, 2024
644367d
Updated code formatting to match rules in .editorconfig
invalid-email-address Dec 16, 2024
f21d004
Updated rosyyn provider
varndellwagglebee Dec 16, 2024
0c277b3
compiler enumerator handling
bfarmer67 Dec 18, 2024
8c01375
Updated code formatting to match rules in .editorconfig
invalid-email-address Dec 18, 2024
888ada3
Added Bracket [] to Key Validator and test, updated docs
varndellwagglebee Dec 19, 2024
d39e28d
Updated nugets
varndellwagglebee Dec 19, 2024
2adfe2f
Updated readme file
varndellwagglebee Dec 19, 2024
9035f5b
small clean up
bfarmer67 Dec 19, 2024
376f1ab
Updated code formatting to match rules in .editorconfig
invalid-email-address Dec 19, 2024
0e59cbf
fix doc duplicate example
bfarmer67 Dec 19, 2024
26d16bf
Merge remote-tracking branch 'origin/feature/13-feature-add-foreach-l…
bfarmer67 Dec 19, 2024
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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,38 @@ var result = parser.Render(template);
Console.WriteLine(result); // Output: 012.
```

### Each Statement

```csharp
var template = "{{each n:x => x.list.Split( \",\" )}}World {{n}},{{/each}}";

var parser = new TemplateParser
{
Variables = { ["list"] = "John,James,Sarah" }
};

var result = parser.Render(template);
Console.WriteLine(result); // hello World John,World James,World Sarah,.
```

```csharp

var template = "{{each n:x => x.Where( t => Regex.IsMatch( t.Key, \"people*\" ) ).Select( t => t.Value )}}hello {{n}}. {{/each}}";

var parser = new TemplateParser
{
Variables =
{
["people[0]"] = "John",
["people[1]"] = "Jane",
["people[2]"] = "Doe"
}
};

var result = parser.Render(template);
Console.WriteLine(result); // hello John. hello Jane. hello Doe.
```

### Methods

You can invoke methods within token expressions.
Expand Down
32 changes: 32 additions & 0 deletions docs/syntax/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,38 @@ var result = parser.Render(template);
Console.WriteLine(result); // Output: 012.
```

### Each Statement

```csharp
var template = "{{each n:x => x.list.Split( \",\" )}}World {{n}},{{/each}}";

var parser = new TemplateParser
{
Variables = { ["list"] = "John,James,Sarah" }
};

var result = parser.Render(template);
Console.WriteLine(result); // hello World John,World James,World Sarah,.
```

```csharp

var template = "{{each n:x => x.Where( t => Regex.IsMatch( t.Key, \"people*\" ) ).Select( t => t.Value )}}hello {{n}}. {{/each}}";

var parser = new TemplateParser
{
Variables =
{
["people[0]"] = "John",
["people[1]"] = "Jane",
["people[2]"] = "Doe"
}
};

var result = parser.Render(template);
Console.WriteLine(result); // hello John. hello Jane. hello Doe.
```

## Inline Definitions

```csharp
Expand Down
6 changes: 6 additions & 0 deletions docs/syntax/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ The `while` statement repeats a template block while a condition is true.

`{{ while condition }} ... {{ /while }}`

### Each Statement

The `each` statement repeats a template block for each a condition.

`{{ each condition }} ... {{ /each }}`

## Inline Declarations

You can declare variable tokens inline within the template.
Expand Down
17 changes: 15 additions & 2 deletions docs/syntax/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,18 @@ nav_order: 1

# Templating Syntax

Hyperbee Templating provides a variety of token syntaxes for different use cases. This section provides a
guide to to the available syntax forms.
Hyperbee Templating is a lightweight templating and variable substitution syntax engine. The library supports value replacements,
code expressions, token nesting, in-line definitions, conditional flow, and looping. It is designed to be lightweight and fast,
and does not rely on any external dependencies.

## Features

* Variable substitution syntax engine
* Value replacements
* Expression replacements
* Token nesting
* Conditional tokens
* Conditional flow
* Iterators
* User-defined methods

96 changes: 67 additions & 29 deletions src/Hyperbee.Templating/Compiler/RoslynTokenExpressionProvider.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.Collections.Concurrent;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Hyperbee.Templating.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand All @@ -16,10 +18,14 @@ internal sealed class RoslynTokenExpressionProvider : ITokenExpressionProvider
private static readonly ImmutableArray<MetadataReference> MetadataReferences =
[
MetadataReference.CreateFromFile( typeof( object ).Assembly.Location ),
MetadataReference.CreateFromFile( typeof( object ).Assembly.Location.Replace( "System.Private.CoreLib", "System.Runtime" ) ),
MetadataReference.CreateFromFile( typeof( RuntimeBinderException ).Assembly.Location ),
MetadataReference.CreateFromFile( typeof( DynamicAttribute ).Assembly.Location ),
MetadataReference.CreateFromFile( typeof( RoslynTokenExpressionProvider ).Assembly.Location )
MetadataReference.CreateFromFile( typeof( RoslynTokenExpressionProvider ).Assembly.Location ),
MetadataReference.CreateFromFile( typeof( Regex ).Assembly.Location ),
MetadataReference.CreateFromFile( typeof( Enumerable ).Assembly.Location ),

MetadataReference.CreateFromFile( typeof( object ).Assembly.Location.Replace( "System.Private.CoreLib", "System.Runtime" ) ),
MetadataReference.CreateFromFile( typeof( IList ).Assembly.Location.Replace( "System.Private.CoreLib", "System.Collections" ) )
];

private sealed class RuntimeContext( ImmutableArray<MetadataReference> metadataReferences )
Expand All @@ -35,33 +41,40 @@ private sealed class RuntimeContext( ImmutableArray<MetadataReference> metadataR
new( OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release );

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

public static void Reset()
{
__runtimeContext = new RuntimeContext( MetadataReferences );
}

private static TokenExpression Compile( string codeExpression )
private static TokenExpression Compile( string codeExpression, MemberDictionary members )
{
// Create a shim to compile the expression
//AF: I added the linq and regular Expression usings
//AF: the error is in the Regex as it doesn't know what the people are.
var codeShim =
$$"""
using Hyperbee.Templating.Text;
using Hyperbee.Templating.Compiler;

public static class TokenExpressionInvoker
{
public static object Invoke( {{nameof( IReadOnlyMemberDictionary )}} members )
{
TokenExpression expr = {{codeExpression}};
return expr( members );
}
}
""";
$$$"""
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Collections;
using System.Collections.Generic;
using Hyperbee.Templating.Text;
using Hyperbee.Templating.Compiler;

public static class TokenExpressionInvoker
{
public static object Invoke( {{{nameof( IReadOnlyMemberDictionary )}}} members )
{
TokenExpression expr = {{{codeExpression}}};
return expr( members );
}
}
""";

// Parse the code expression
var syntaxTree = CSharpSyntaxTree.ParseText( codeShim );
Expand All @@ -80,10 +93,10 @@ public static object Invoke( {{nameof( IReadOnlyMemberDictionary )}} members )
};

// Rewrite the lambda expression to use the dictionary lookup
var rewriter = new TokenExpressionRewriter( parameterName );
var rewriter = new TokenExpressionRewriter( parameterName, members );
var rewrittenSyntaxTree = rewriter.Visit( root );

//var rewrittenCode = rewrittenSyntaxTree.ToFullString(); // Keep for debugging
var rewrittenCode = rewrittenSyntaxTree.ToFullString(); // Keep for debugging

// Compile the rewritten code
var counter = Interlocked.Increment( ref __counter );
Expand All @@ -101,9 +114,9 @@ public static object Invoke( {{nameof( IReadOnlyMemberDictionary )}} members )
{
var failures = result.Diagnostics.Where( diagnostic =>
diagnostic.IsWarningAsError ||
diagnostic.Severity == DiagnosticSeverity.Error );
diagnostic.Severity == DiagnosticSeverity.Error ).ToArray();

throw new InvalidOperationException( "Compilation failed: " + string.Join( "\n", failures.Select( diagnostic => diagnostic.GetMessage() ) ) );
throw new TokenExpressionProviderException( "Compilation failed: " + failures[0]?.GetMessage(), failures );
}

peStream.Seek( 0, SeekOrigin.Begin );
Expand All @@ -118,16 +131,36 @@ public static object Invoke( {{nameof( IReadOnlyMemberDictionary )}} members )
}
}

[Serializable]
internal class TokenExpressionProviderException : Exception
{
public Diagnostic[] Diagnostic { get; }
public string Id => Diagnostic != null && Diagnostic.Length > 0 ? Diagnostic[0].Id : string.Empty;

public TokenExpressionProviderException( string message, Diagnostic[] diagnostic )
: base( message )
{
Diagnostic = diagnostic;
}

public TokenExpressionProviderException( string message, Diagnostic[] diagnostic, Exception innerException )
: base( message, innerException )
{
Diagnostic = diagnostic;
}

}

// This rewriter will transform the lambda expression to use dictionary lookup
// for property access, method invocation, and 'generic' property casting.
//
// we want to transform these syntactic-sugar patterns:
//
// 1. x => x.someProp to x["someProp"]
// 2. x => x.someProp<T> to x.GetValueAs<T>("someProp")
// 3. x => x.someMethod(..) to x.InvokeMethod("someMethod", ..)
// 3. x => x.someMethod(..) to x.Invoke("someMethod", ..)

internal class TokenExpressionRewriter( string parameterName ) : CSharpSyntaxRewriter
internal class TokenExpressionRewriter( string parameterName, MemberDictionary members ) : CSharpSyntaxRewriter
{
private readonly HashSet<string> _aliases = [parameterName];

Expand All @@ -151,7 +184,12 @@ memberAccess.Expression is IdentifierNameSyntax identifier &&
_aliases.Contains( identifier.Identifier.Text ) )
{
// Handle method invocation rewrite
return RewriteMethodInvocation( memberAccess, node );

if ( members.Methods.ContainsKey( memberAccess.Name.Identifier.Text ) )
return RewriteMethodInvocation( memberAccess, node );

return node.Update( node.Expression, (ArgumentListSyntax) VisitArgumentList( node.ArgumentList )! );
//return RewriteMethodInvocation( memberAccess, node ); //BF this rewrite causes the error. we need to disambiguate calls to template lambdas methods
}

return base.VisitInvocationExpression( node );
Expand Down Expand Up @@ -232,12 +270,12 @@ private InvocationExpressionSyntax RewriteMethodInvocation( MemberAccessExpressi
.Select( arg => (ExpressionSyntax) Visit( arg.Expression ) )
.ToArray();

// Create the InvokeMethod call: x.InvokeMethod("MethodName", arg1, arg2, ...)
// Create the InvokeMethod call: x.Invoke("MethodName", arg1, arg2, ...)
var invokeMethodCall = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
memberAccess.Expression, // This is `x`
SyntaxFactory.IdentifierName( "InvokeMethod" )
SyntaxFactory.IdentifierName( "Invoke" )
),
SyntaxFactory.ArgumentList(
SyntaxFactory.SeparatedList(
Expand Down
2 changes: 1 addition & 1 deletion src/Hyperbee.Templating/Compiler/TokenExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ namespace Hyperbee.Templating.Compiler;

public interface ITokenExpressionProvider
{
public TokenExpression GetTokenExpression( string codeExpression );
public TokenExpression GetTokenExpression( string codeExpression, MemberDictionary members );
}
3 changes: 2 additions & 1 deletion src/Hyperbee.Templating/Configure/TemplateOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.ComponentModel;
using System.Reflection;
using Hyperbee.Templating.Compiler;
using Hyperbee.Templating.Core;
using Hyperbee.Templating.Text;

namespace Hyperbee.Templating.Configure;
Expand All @@ -13,7 +14,7 @@ public class TemplateOptions
public IDictionary<string, string> Variables { get; init; }

public TokenStyle TokenStyle { get; set; } = TokenStyle.Default;
public KeyValidator Validator { get; set; } = TemplateHelper.ValidateKey;
public KeyValidator Validator { get; set; } = KeyHelper.ValidateKey;

public bool IgnoreMissingTokens { get; set; }
public bool SubstituteEnvironmentVariables { get; set; }
Expand Down
27 changes: 27 additions & 0 deletions src/Hyperbee.Templating/Core/EnumeratorAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Collections;

namespace Hyperbee.Templating.Core;

internal sealed class EnumeratorAdapter : IEnumerator<string>
{
private readonly IEnumerator _inner;

internal EnumeratorAdapter( IEnumerable enumerable )
{
if ( enumerable is not IEnumerable<IConvertible> typedEnumerable )
throw new ArgumentException( "The enumerable must be of type IEnumerable<IConvertible>.", nameof( enumerable ) );

// take a snapshot of the enumerable to prevent changes during enumeration
var snapshot = new List<IConvertible>( typedEnumerable );

// ReSharper disable once GenericEnumeratorNotDisposed
_inner = snapshot.GetEnumerator();
}

public string Current => (string) _inner.Current;
object IEnumerator.Current => _inner.Current;

public bool MoveNext() => _inner.MoveNext();
public void Reset() => _inner.Reset();
public void Dispose() => (_inner as IDisposable)?.Dispose();
}
55 changes: 55 additions & 0 deletions src/Hyperbee.Templating/Core/KeyHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace Hyperbee.Templating.Core;

public delegate bool KeyValidator( ReadOnlySpan<char> key );

internal static class KeyHelper
{
public static bool ValidateKey( string key )
{
// do-not-remove this method.
//
// this method is required despite code analysis claiming the method isn't referenced.
//
// this overload is required (and used) by generic delegates which don't support
// ReadOnlySpan<char> as a generic argument.

return ValidateKey( key.AsSpan() );
}

public static bool ValidateKey( ReadOnlySpan<char> key )
{
if ( key.IsEmpty || !char.IsLetter( key[0] ) )
{
return false;
}

var length = key.Length;

for ( var i = 1; i < length; i++ )
{
var current = key[i];

if ( current == '[' )
{
if ( ++i >= length || !char.IsDigit( key[i] ) )
return false;

while ( i < length && char.IsDigit( key[i] ) )
i++;

if ( i >= length || key[i] != ']' )
return false;

// Ensure that the bracket is at the end of the string
if ( i != length - 1 )
return false;
}
else if ( !char.IsLetterOrDigit( current ) && current != '_' )
{
return false;
}
}

return true;
}
}
Loading
Loading