From f3f69250ac717b46b5da0219f33795d822ed99c5 Mon Sep 17 00:00:00 2001 From: Aiden Fuller Date: Sat, 1 Mar 2025 11:41:40 +1100 Subject: [PATCH] Added support for includeZero hash parameter into inbuilt conditional blocks --- .../Handlebars.Test/BasicIntegrationTests.cs | 32 +++++++++++++++++++ .../ConditionalBlockAccumulatorContext.cs | 25 +++++++++++++-- .../Compiler/Structure/BoolishExpression.cs | 5 ++- .../Structure/HandlebarsExpression.cs | 4 +-- .../Expression/BoolishConverter.cs | 17 ++++++++-- .../Expression/HandlebarsExpressionVisitor.cs | 3 +- source/Handlebars/HandlebarsUtils.cs | 20 ++++++------ 7 files changed, 89 insertions(+), 17 deletions(-) diff --git a/source/Handlebars.Test/BasicIntegrationTests.cs b/source/Handlebars.Test/BasicIntegrationTests.cs index bc942451..e6b9b659 100644 --- a/source/Handlebars.Test/BasicIntegrationTests.cs +++ b/source/Handlebars.Test/BasicIntegrationTests.cs @@ -1439,6 +1439,38 @@ public void BasicNumericTruthy(IHandlebars handlebars) Assert.Equal("Hello, Truthy!", result); } + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicNumericZeroWhenIncludeZeroTrue(IHandlebars handlebars) + { + string source = "Hello, {{#if zero includeZero=true}}Truthy!{{/if}}"; + + var template = handlebars.Compile(source); + + var data = new + { + zero = 0 + }; + + var result = template(data); + Assert.Equal("Hello, Truthy!", result); + } + + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] + public void BasicNumericZeroWhenIncludeZeroFalse(IHandlebars handlebars) + { + string source = "Hello, {{#if zero includeZero=false}}Truthy!{{/if}}"; + + var template = handlebars.Compile(source); + + var data = new + { + zero = 0 + }; + + var result = template(data); + Assert.Equal("Hello, ", result); + } + [Theory, ClassData(typeof(HandlebarsEnvGenerator))] public void BasicStringFalsy(IHandlebars handlebars) { diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs index 57e128e5..297bff2e 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/ConditionalBlockAccumulatorContext.cs @@ -30,7 +30,8 @@ public ConditionalBlockAccumulatorContext(Expression startingNode) throw new HandlebarsCompilerException($"Tried to convert {BlockName} expression to conditional block", helperExpression.Context); } - var argument = HandlebarsExpression.Boolish(helperExpression.Arguments.Single()); + var (value, hashParameters) = UnwrapBoolishHelperArguments(helperExpression.Arguments, helperExpression.Context); + var argument = HandlebarsExpression.Boolish(value, hashParameters); _currentCondition = BlockName switch { @@ -115,7 +116,9 @@ private bool IsElseIfBlock(Expression item) private Expression GetElseIfTestExpression(Expression item) { item = UnwrapStatement(item); - return HandlebarsExpression.Boolish(((HelperExpression)item).Arguments.Skip(1).Single()); + var helperExpression = (HelperExpression)item; + var (value, hashParameters) = UnwrapBoolishHelperArguments(helperExpression.Arguments.Skip(1), helperExpression.Context); + return HandlebarsExpression.Boolish(value, hashParameters); } private bool IsClosingNode(Expression item) @@ -133,6 +136,24 @@ private static Expression SinglifyExpressions(IEnumerable expression return expressions.SingleOrDefault() ?? Expression.Empty(); } + + private static (Expression Value, HashParametersExpression HashParameters) UnwrapBoolishHelperArguments(IEnumerable arguments, IReaderContext context) + { + var value = arguments.First(); + if (arguments.Count() == 1) + { + var blankHashParameters = HandlebarsExpression.HashParametersExpression(new Dictionary()); + return (value, blankHashParameters); + } + + if (arguments.Count() == 2 && arguments.Last() is HashParametersExpression hashParameters) + { + return (value, hashParameters); + } + + throw new HandlebarsCompilerException("Invalid argument list for conditional block.", context); + + } } } diff --git a/source/Handlebars/Compiler/Structure/BoolishExpression.cs b/source/Handlebars/Compiler/Structure/BoolishExpression.cs index 285211a5..0e2fdb3f 100644 --- a/source/Handlebars/Compiler/Structure/BoolishExpression.cs +++ b/source/Handlebars/Compiler/Structure/BoolishExpression.cs @@ -5,13 +5,16 @@ namespace HandlebarsDotNet.Compiler { internal class BoolishExpression : HandlebarsExpression { - public BoolishExpression(Expression condition) + public BoolishExpression(Expression condition, HashParametersExpression hashParameters) { Condition = condition; + HashParameters = hashParameters; } public new Expression Condition { get; } + public HashParametersExpression HashParameters { get; set; } + public override ExpressionType NodeType => (ExpressionType)HandlebarsExpressionType.BoolishExpression; public override Type Type => typeof(bool); diff --git a/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs b/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs index 7830834c..d7ceb249 100644 --- a/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs +++ b/source/Handlebars/Compiler/Structure/HandlebarsExpression.cs @@ -102,9 +102,9 @@ public static PartialExpression Partial(Expression partialName, Expression argum return new PartialExpression(partialName, argument, fallback); } - public static BoolishExpression Boolish(Expression condition) + public static BoolishExpression Boolish(Expression condition, HashParametersExpression hashParameters) { - return new BoolishExpression(condition); + return new BoolishExpression(condition, hashParameters); } public static SubExpressionExpression SubExpression(Expression expression) diff --git a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs index 3b38d409..0040dcd9 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq.Expressions; using static Expressions.Shortcuts.ExpressionShortcuts; @@ -11,13 +13,24 @@ public BoolishConverter(CompilationContext compilationContext) { _compilationContext = compilationContext; } - + + private const string IncludeZero = "includeZero"; + protected override Expression VisitBoolishExpression(BoolishExpression bex) { var condition = Visit(bex.Condition); condition = FunctionBuilder.Reduce(condition, _compilationContext, out _); + var hashParameters = (HashParametersExpression)VisitHashParametersExpression(bex.HashParameters); + // Assert that if there is a hashParameter, it is "includeZero" and has a boolean value + if (hashParameters.Parameters.Count > 1 || (hashParameters.Parameters.Count == 1 && !hashParameters.Parameters.ContainsKey(IncludeZero))) + { + throw new HandlebarsCompilerException($"Boolish expression can only have up to one hash parameter, '{IncludeZero}'"); + } + var @object = Arg(condition); - return Call(() => HandlebarsUtils.IsTruthyOrNonEmpty(@object)); + var includeZero = Arg(hashParameters.Parameters.Count == 1 ? hashParameters.Parameters[IncludeZero] : Expression.Constant(false)); + return Call(() => HandlebarsUtils.IsTruthyOrNonEmpty(@object, includeZero)); + } } } diff --git a/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs index 32cb4949..7cf019f4 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HandlebarsExpressionVisitor.cs @@ -112,9 +112,10 @@ protected virtual Expression VisitPartialExpression(PartialExpression pex) protected virtual Expression VisitBoolishExpression(BoolishExpression bex) { Expression condition = Visit(bex.Condition); + HashParametersExpression hashParameters = (HashParametersExpression)VisitHashParametersExpression(bex.HashParameters); if (condition != bex.Condition) { - return HandlebarsExpression.Boolish(condition); + return HandlebarsExpression.Boolish(condition, hashParameters); } return bex; } diff --git a/source/Handlebars/HandlebarsUtils.cs b/source/Handlebars/HandlebarsUtils.cs index db1c453f..f940aa15 100644 --- a/source/Handlebars/HandlebarsUtils.cs +++ b/source/Handlebars/HandlebarsUtils.cs @@ -10,18 +10,20 @@ public static class HandlebarsUtils /// Implementation of JS's `==` /// /// + /// Determined whether the numerical 0 is considered to be truthy. Default false /// - public static bool IsTruthy(object value) + public static bool IsTruthy(object value, bool includeZero = false) { - return !IsFalsy(value); + return !IsFalsy(value, includeZero); } /// /// Implementation of JS's `!=` /// - /// + /// The value whose falsy-ness is to be determined + /// Determined whether the numerical 0 is considered to be truthy /// - public static bool IsFalsy(object value) + public static bool IsFalsy(object value, bool includeZero) { switch (value) { @@ -34,21 +36,21 @@ public static bool IsFalsy(object value) return s == string.Empty; } - if (IsNumber(value)) + if (IsNumber(value) && !includeZero) { return !Convert.ToBoolean(value); } return false; } - public static bool IsTruthyOrNonEmpty(object value) + public static bool IsTruthyOrNonEmpty(object value, bool includeZero = false) { - return !IsFalsyOrEmpty(value); + return !IsFalsyOrEmpty(value, includeZero); } - public static bool IsFalsyOrEmpty(object value) + public static bool IsFalsyOrEmpty(object value, bool includeZero = false) { - if(IsFalsy(value)) + if(IsFalsy(value, includeZero)) { return true; }