Skip to content

Commit c4bb82c

Browse files
authored
Add String.First/Last helpers and fix Split for multi-char separators (#135)
Fix String.Split to correctly handle multi-character separators like "<br />". Previously, it would split on each individual character rather than the whole string. Add String.First and String.Last helpers to extract the first or last element from an array/collection. These can be composed with Split: {{String.First (String.Split yourField "<br />")}} Changes: - Fix Split to use string[] overload instead of char[] for multi-char separators - Add First(IEnumerable<object?>) returning the first element - Add Last(IEnumerable<object?>) returning the last element - Add unit and template tests for all changes - Update CHANGELOG for 2.5.4
1 parent 13e6b24 commit c4bb82c

4 files changed

Lines changed: 163 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 2.5.4 (TBD)
2+
- Fix String.Split to correctly handle multi-character separators (e.g., `"<br />"` now splits on the whole string instead of each character) [bug]
3+
- Add String.First helper to get the first element from an array/collection [enhancement]
4+
- Add String.Last helper to get the last element from an array/collection [enhancement]
5+
16
# 2.5.3 (13 September 2025)
27
- [#132](https://github.com/Handlebars-Net/Handlebars.Net.Helpers/pull/132) - Humanizer Truncate should always be used as &quot;Humanizer.Truncate&quot; [bug] contributed by [StefH](https://github.com/StefH)
38
- [#133](https://github.com/Handlebars-Net/Handlebars.Net.Helpers/pull/133) - Fix SonarCloud [enhancement] contributed by [StefH](https://github.com/StefH)

src/Handlebars.Net.Helpers/Helpers/StringHelpers.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections;
3+
using System.Collections.Generic;
34
using System.Linq;
45
using System.Text;
56
using HandlebarsDotNet.Helpers.Attributes;
@@ -342,13 +343,35 @@ public string Reverse(string value)
342343
return new string(value.ToCharArray().Reverse().ToArray());
343344
}
344345

346+
[HandlebarsWriter(WriterType.Value)]
347+
public object? First(IEnumerable<object?> values)
348+
{
349+
if (values is null)
350+
{
351+
throw new ArgumentNullException(nameof(values));
352+
}
353+
354+
return values.FirstOrDefault();
355+
}
356+
357+
[HandlebarsWriter(WriterType.Value)]
358+
public object? Last(IEnumerable<object?> values)
359+
{
360+
if (values is null)
361+
{
362+
throw new ArgumentNullException(nameof(values));
363+
}
364+
365+
return values.LastOrDefault();
366+
}
367+
345368
[HandlebarsWriter(WriterType.Value)]
346369
public string[] Split(string value, string separator)
347370
{
348371
Guard.NotNull(value);
349372
Guard.NotNull(separator);
350373

351-
return separator.Length == 1 ? value.Split(separator[0]) : value.Split(separator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
374+
return value.Split(new[] { separator }, StringSplitOptions.None);
352375
}
353376

354377
[HandlebarsWriter(WriterType.Value)]

test/Handlebars.Net.Helpers.Tests/Helpers/StringHelpersTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,4 +598,90 @@ public void Substring_3params_Exceptions(string value, int start, int end)
598598
// Assert
599599
action.Should().Throw<ArgumentException>();
600600
}
601+
602+
[Theory]
603+
[InlineData("a;b;c", ";", new[] { "a", "b", "c" })]
604+
[InlineData("a<br />b<br />c", "<br />", new[] { "a", "b", "c" })]
605+
[InlineData("Honors Algebra 2<br /><br />More text", "<br />", new[] { "Honors Algebra 2", "", "More text" })]
606+
[InlineData("no separator here", ";", new[] { "no separator here" })]
607+
public void Split(string value, string separator, string[] expected)
608+
{
609+
// Act
610+
var result = _sut.Split(value, separator);
611+
612+
// Assert
613+
result.Should().BeEquivalentTo(expected);
614+
}
615+
616+
[Fact]
617+
public void First_ReturnsFirstElement()
618+
{
619+
// Arrange
620+
var values = new object[] { "first", "second", "third" };
621+
622+
// Act
623+
var result = _sut.First(values);
624+
625+
// Assert
626+
result.Should().Be("first");
627+
}
628+
629+
[Fact]
630+
public void First_ReturnsNullForEmptyCollection()
631+
{
632+
// Arrange
633+
var values = Array.Empty<object>();
634+
635+
// Act
636+
var result = _sut.First(values);
637+
638+
// Assert
639+
result.Should().BeNull();
640+
}
641+
642+
[Fact]
643+
public void First_ThrowsForNullCollection()
644+
{
645+
// Act
646+
Action action = () => _sut.First(null!);
647+
648+
// Assert
649+
action.Should().Throw<ArgumentNullException>();
650+
}
651+
652+
[Fact]
653+
public void Last_ReturnsLastElement()
654+
{
655+
// Arrange
656+
var values = new object[] { "first", "second", "third" };
657+
658+
// Act
659+
var result = _sut.Last(values);
660+
661+
// Assert
662+
result.Should().Be("third");
663+
}
664+
665+
[Fact]
666+
public void Last_ReturnsNullForEmptyCollection()
667+
{
668+
// Arrange
669+
var values = Array.Empty<object>();
670+
671+
// Act
672+
var result = _sut.Last(values);
673+
674+
// Assert
675+
result.Should().BeNull();
676+
}
677+
678+
[Fact]
679+
public void Last_ThrowsForNullCollection()
680+
{
681+
// Act
682+
Action action = () => _sut.Last(null!);
683+
684+
// Assert
685+
action.Should().Throw<ArgumentNullException>();
686+
}
601687
}

test/Handlebars.Net.Helpers.Tests/Templates/StringHelpersTemplateTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,4 +596,52 @@ public void FormatAsString_Template_Now()
596596
decodeResult.Should().BeTrue();
597597
decoded.Should().Be("test 2020-Apr-15 abc");
598598
}
599+
600+
[Theory]
601+
[InlineData("{{String.First (String.Split \"a<br />b<br />c\" \"<br />\")}}", "a")]
602+
[InlineData("{{String.First (String.Split \"Honors Algebra 2<br /><br />More text\" \"<br />\")}}", "Honors Algebra 2")]
603+
[InlineData("{{String.First (String.Split \"single\" \";\")}}", "single")]
604+
public void FirstWithSplit(string template, string expected)
605+
{
606+
// Arrange
607+
var action = _handlebarsContext.Compile(template);
608+
609+
// Act
610+
var result = action("");
611+
612+
// Assert
613+
result.Should().Be(expected);
614+
}
615+
616+
[Theory]
617+
[InlineData("{{String.Last (String.Split \"a<br />b<br />c\" \"<br />\")}}", "c")]
618+
[InlineData("{{String.Last (String.Split \"single\" \";\")}}", "single")]
619+
public void LastWithSplit(string template, string expected)
620+
{
621+
// Arrange
622+
var action = _handlebarsContext.Compile(template);
623+
624+
// Act
625+
var result = action("");
626+
627+
// Assert
628+
result.Should().Be(expected);
629+
}
630+
631+
[Fact]
632+
public void FirstWithSplit_FromModel()
633+
{
634+
// Arrange
635+
var model = new
636+
{
637+
yourField = "Honors Algebra 2<br /><br /><span style=\"background-color: #fbeeb8;\"><strong>Textbook<br /></strong>Algebra and Trigonometry</span>"
638+
};
639+
var action = _handlebarsContext.Compile("{{String.First (String.Split yourField \"<br />\")}}");
640+
641+
// Act
642+
var result = action(model);
643+
644+
// Assert
645+
result.Should().Be("Honors Algebra 2");
646+
}
599647
}

0 commit comments

Comments
 (0)