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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,44 @@ var result = template(data);
*/
```

### Multi-dimensional Arrays

Handlebars.Net supports indexing and iterating true multi-dimensional (rank > 1) .NET arrays, such as `int[,]` or `int[,,]`, in addition to jagged arrays (`int[][]`) and other list/enumerable types.

A multi-dimensional array is indexed and iterated one dimension at a time, so a 2D array is treated as an array of rows and a 3D array as an array of 2D "slabs", and so on:

```c#
string source = "{{#each grid}}[{{#each this}}{{this}}{{/each}}]{{/each}}";

var template = Handlebars.Compile(source);

var data = new {
grid = new int[,] { { 1, 2, 3 }, { 4, 5, 6 } }
};

var result = template(data);

/* Would render:
[123][456]
*/
```

Individual elements can also be reached directly by chaining index segments, one per dimension:

```c#
string source = "{{ grid.[1].[2] }}"; // grid[1, 2]

var template = Handlebars.Compile(source);

var data = new {
grid = new int[,] { { 1, 2, 3 }, { 4, 5, 6 } }
};

var result = template(data); // "6"
```

This is a C#-specific capability, since JavaScript/Handlebars.js has no equivalent to true multi-dimensional arrays.

### Registering Partials

```c#
Expand Down
43 changes: 43 additions & 0 deletions source/Handlebars.Test/BasicIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,49 @@ public void BasicPathArrayNoSquareBracketsChildPath(IHandlebars handlebars)
Assert.Equal("Hello, Handlebars.Net!", result);
}

[Theory, ClassData(typeof(HandlebarsEnvGenerator))]
public void BasicPathJaggedArray(IHandlebars handlebars)
{
var source = "{{ grid.[1].[2] }}";
var template = handlebars.Compile(source);
var data = new
{
grid = new[] { new[] { 1, 2, 3 }, new[] { 4, 5, 6 } }
};
var result = template(data);
Assert.Equal("6", result);
}

[Theory, ClassData(typeof(HandlebarsEnvGenerator))]
public void BasicPathTwoDimensionalArray(IHandlebars handlebars)
{
var source = "{{ grid.[1].[2] }}";
var template = handlebars.Compile(source);
var data = new
{
grid = new int[,] { { 1, 2, 3 }, { 4, 5, 6 } }
};
var result = template(data);
Assert.Equal("6", result);
}

[Theory, ClassData(typeof(HandlebarsEnvGenerator))]
public void BasicPathThreeDimensionalArray(IHandlebars handlebars)
{
var source = "{{ cube.[1].[0].[1] }}";
var template = handlebars.Compile(source);
var data = new
{
cube = new int[,,]
{
{ { 1, 2 }, { 3, 4 } },
{ { 5, 6 }, { 7, 8 } }
}
};
var result = template(data);
Assert.Equal("6", result);
}

[Theory, ClassData(typeof(HandlebarsEnvGenerator))]
public void BasicPathEnumerableNoSquareBracketsChildPath(IHandlebars handlebars)
{
Expand Down
56 changes: 56 additions & 0 deletions source/Handlebars.Test/IteratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,62 @@ public void ImmutableArrayTest()
var result = template(data);
Assert.Equal("0123", result);
}

[Fact]
public void JaggedArrayIterator()
{
var source = "{{#each data}}[{{#each this}}{{this}}{{/each}}]{{/each}}";
var template = Handlebars.Compile(source);
var data = new
{
data = new[] { new[] { 1, 2 }, new[] { 3, 4, 5 } }
};
var result = template(data);
Assert.Equal("[12][345]", result);
}

[Fact]
public void TwoDimensionalArrayIteratorIteratesByRow()
{
var source = "{{#each data}}[{{#each this}}{{this}}{{/each}}]{{/each}}";
var template = Handlebars.Compile(source);
var data = new
{
data = new int[,] { { 1, 2, 3 }, { 4, 5, 6 } }
};
var result = template(data);
Assert.Equal("[123][456]", result);
}

[Fact]
public void TwoDimensionalArrayIteratorWithIndex()
{
var source = "{{#each data}}{{@index}}:{{#each this}}{{@index}}={{this}} {{/each}}\n{{/each}}";
var template = Handlebars.Compile(source);
var data = new
{
data = new int[,] { { 1, 2 }, { 3, 4 } }
};
var result = template(data);
Assert.Equal("0:0=1 1=2 \n1:0=3 1=4 \n", result);
}

[Fact]
public void ThreeDimensionalArrayIterator()
{
var source = "{{#each data}}{{#each this}}[{{#each this}}{{this}}{{/each}}]{{/each}}\n{{/each}}";
var template = Handlebars.Compile(source);
var data = new
{
data = new int[,,]
{
{ { 1, 2 }, { 3, 4 } },
{ { 5, 6 }, { 7, 8 } }
}
};
var result = template(data);
Assert.Equal("[12][34]\n[56][78]\n", result);
}
}
}

67 changes: 67 additions & 0 deletions source/Handlebars/Iterators/MultidimensionalArrayIterator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using HandlebarsDotNet.Compiler;
using HandlebarsDotNet.PathStructure;
using HandlebarsDotNet.Runtime;
using HandlebarsDotNet.ValueProviders;

namespace HandlebarsDotNet.Iterators
{
/// <summary>
/// Iterates a true multi-dimensional <see cref="Array"/> (rank > 1) one row at a time,
/// i.e. by its outer-most dimension. Each yielded value is a <see cref="MultidimensionalArraySlice"/>
/// covering the remaining dimensions, so nested dimensions can be walked with further
/// <c>{{#each}}</c> blocks or indexers.
/// </summary>
public sealed class MultidimensionalArrayIterator : IIterator
{
public void Iterate(
in EncodedTextWriter writer,
BindingContext context,
ChainSegment[] blockParamsVariables,
object input,
TemplateDelegate template,
TemplateDelegate ifEmpty
)
{
using var innerContext = context.CreateFrame();
var iterator = new IteratorValues(innerContext);
var blockParamsValues = new BlockParamsValues(innerContext, blockParamsVariables);

blockParamsValues.CreateProperty(0, out var _0);
blockParamsValues.CreateProperty(1, out var _1);

var array = (Array) input;
var count = array.GetLength(0);

iterator.First = BoxedValues.True;
iterator.Last = BoxedValues.False;

var index = 0;
var lastIndex = count - 1;
for (; index < count; index++)
{
var value = (object?) new MultidimensionalArraySlice(array, new[] { index });
var objectIndex = BoxedValues.Int(index);

if (index == 1) iterator.First = BoxedValues.False;
if (index == lastIndex) iterator.Last = BoxedValues.True;

iterator.Key = iterator.Index = objectIndex;

blockParamsValues[_0] = value;
blockParamsValues[_1] = objectIndex;

iterator.Value = value;
innerContext.Value = value;

template(writer, innerContext);
}

if (index == 0)
{
innerContext.Value = context.Value;
ifEmpty(writer, innerContext);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ public class EnumerableMemberAccessor : IMemberAccessor
{
public static EnumerableMemberAccessor Create(Type type)
{
if (type.IsArray && type.GetArrayRank() > 1)
{
return new MultiDimensionalArrayMemberAccessor();
}

if (type.IsAssignableToGenericType(typeof(IList<>), out var genericType))
{
var typeArgument = genericType.GenericTypeArguments[0];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using HandlebarsDotNet.Runtime;

namespace HandlebarsDotNet.MemberAccessors.EnumerableAccessors
{
public sealed class MultiDimensionalArrayMemberAccessor : EnumerableMemberAccessor
{
protected override bool TryGetValueInternal(object instance, int index, out object? value)
{
var array = (Array) instance;
if (index >= array.GetLength(0))
{
value = null;
return false;
}

value = new MultidimensionalArraySlice(array, new[] { index });
return true;
}
}
}
20 changes: 14 additions & 6 deletions source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,25 @@ public bool TryGetDescriptor(Type type, [MaybeNullWhen(false)] out ObjectDescrip

private static bool TryCreateArrayDescriptor(Type type, object[] parameters, [MaybeNullWhen(false)] out ObjectDescriptor value)
{
if (type.IsArray)
if (!type.IsArray)
{
value = (ObjectDescriptor) ArrayObjectDescriptorFactoryMethodInfo
.MakeGenericMethod(type.GetElementType()!)
.Invoke(null, parameters)!;
value = ObjectDescriptor.Empty;
return false;
}

if (type.GetArrayRank() > 1)
{
var accessor = (IMemberAccessor) parameters[0];
var descriptor = (ObjectDescriptor) parameters[1];
value = new ObjectDescriptor(type, accessor, descriptor.GetProperties, self => new MultidimensionalArrayIterator(), descriptor.Dependencies);
return true;
}

value = ObjectDescriptor.Empty;
return false;
value = (ObjectDescriptor) ArrayObjectDescriptorFactoryMethodInfo
.MakeGenericMethod(type.GetElementType()!)
.Invoke(null, parameters)!;

return true;
}

private static bool TryCreateDescriptorFromOpenGeneric(Type type, Type openGenericType, object[] parameters, MethodInfo method, [MaybeNullWhen(false)] out ObjectDescriptor descriptor)
Expand Down
53 changes: 53 additions & 0 deletions source/Handlebars/Runtime/MultidimensionalArraySlice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Collections;
using System.Collections.Generic;

namespace HandlebarsDotNet.Runtime
{
/// <summary>
/// Represents a lazy view over one dimension of a true multi-dimensional <see cref="Array"/>
/// (rank > 1, e.g. <c>int[,]</c>), fixing the leading indices supplied so far.
/// Indexing or iterating a slice either yields the element (once every dimension has been
/// fixed) or a narrower <see cref="MultidimensionalArraySlice"/> for the remaining dimensions.
/// This lets a rank-N array be walked one dimension at a time, e.g. <c>{{grid.[0].[1]}}</c>
/// or nested <c>{{#each}}</c> blocks, without ever needing to cast it to <c>T[]</c>.
/// </summary>
public sealed class MultidimensionalArraySlice : IReadOnlyList<object?>
{
private readonly Array _array;
private readonly int[] _indices;

internal MultidimensionalArraySlice(Array array, int[] indices)
{
_array = array;
_indices = indices;
}

public int Count => _array.GetLength(_indices.Length);

public object? this[int index]
{
get
{
var indices = new int[_indices.Length + 1];
Array.Copy(_indices, indices, _indices.Length);
indices[_indices.Length] = index;

return indices.Length == _array.Rank
? _array.GetValue(indices)
: new MultidimensionalArraySlice(_array, indices);
}
}

public IEnumerator<object?> GetEnumerator()
{
var count = Count;
for (var index = 0; index < count; index++)
{
yield return this[index];
}
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
Loading