diff --git a/README.md b/README.md
index dddcd227..d9bbd253 100644
--- a/README.md
+++ b/README.md
@@ -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#
diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs
index 3f655750..0f0ae3f5 100644
--- a/source/Handlebars.Test/BasicIntegrationTests.cs
+++ b/source/Handlebars.Test/BasicIntegrationTests.cs
@@ -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)
{
diff --git a/source/Handlebars.Test/IteratorTests.cs b/source/Handlebars.Test/IteratorTests.cs
index 10b6e2d8..f60c9f22 100644
--- a/source/Handlebars.Test/IteratorTests.cs
+++ b/source/Handlebars.Test/IteratorTests.cs
@@ -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);
+ }
}
}
diff --git a/source/Handlebars/Iterators/MultidimensionalArrayIterator.cs b/source/Handlebars/Iterators/MultidimensionalArrayIterator.cs
new file mode 100644
index 00000000..a600feb9
--- /dev/null
+++ b/source/Handlebars/Iterators/MultidimensionalArrayIterator.cs
@@ -0,0 +1,67 @@
+using System;
+using HandlebarsDotNet.Compiler;
+using HandlebarsDotNet.PathStructure;
+using HandlebarsDotNet.Runtime;
+using HandlebarsDotNet.ValueProviders;
+
+namespace HandlebarsDotNet.Iterators
+{
+ ///
+ /// Iterates a true multi-dimensional (rank > 1) one row at a time,
+ /// i.e. by its outer-most dimension. Each yielded value is a
+ /// covering the remaining dimensions, so nested dimensions can be walked with further
+ /// {{#each}} blocks or indexers.
+ ///
+ 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);
+ }
+ }
+ }
+}
diff --git a/source/Handlebars/MemberAccessors/EnumerableAccessors/EnumerableMemberAccessor.cs b/source/Handlebars/MemberAccessors/EnumerableAccessors/EnumerableMemberAccessor.cs
index 32c10c72..759b1f06 100644
--- a/source/Handlebars/MemberAccessors/EnumerableAccessors/EnumerableMemberAccessor.cs
+++ b/source/Handlebars/MemberAccessors/EnumerableAccessors/EnumerableMemberAccessor.cs
@@ -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];
diff --git a/source/Handlebars/MemberAccessors/EnumerableAccessors/MultiDimensionalArrayMemberAccessor.cs b/source/Handlebars/MemberAccessors/EnumerableAccessors/MultiDimensionalArrayMemberAccessor.cs
new file mode 100644
index 00000000..3a32d14e
--- /dev/null
+++ b/source/Handlebars/MemberAccessors/EnumerableAccessors/MultiDimensionalArrayMemberAccessor.cs
@@ -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;
+ }
+ }
+}
diff --git a/source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs b/source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs
index 6face1c9..f8829025 100644
--- a/source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs
+++ b/source/Handlebars/ObjectDescriptors/EnumerableObjectDescriptor.cs
@@ -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)
diff --git a/source/Handlebars/Runtime/MultidimensionalArraySlice.cs b/source/Handlebars/Runtime/MultidimensionalArraySlice.cs
new file mode 100644
index 00000000..222e9960
--- /dev/null
+++ b/source/Handlebars/Runtime/MultidimensionalArraySlice.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace HandlebarsDotNet.Runtime
+{
+ ///
+ /// Represents a lazy view over one dimension of a true multi-dimensional
+ /// (rank > 1, e.g. int[,]), 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 for the remaining dimensions.
+ /// This lets a rank-N array be walked one dimension at a time, e.g. {{grid.[0].[1]}}
+ /// or nested {{#each}} blocks, without ever needing to cast it to T[].
+ ///
+ public sealed class MultidimensionalArraySlice : IReadOnlyList