From 5a72de2600c1dfa0ea2d16856e7531a64102b576 Mon Sep 17 00:00:00 2001 From: Khayyam Saleem Date: Sat, 28 Jan 2023 12:21:22 -0500 Subject: [PATCH 1/2] tpl: adds `truth` and `bool` template functions The behavior of `truth` and `bool` is described in the corresponding test cases and examples. The decision-making around the behavior is a based on combination of the existing behavior of strconv.ParseBool in go and the MDN definition of "truthy" as JavaScript has the most interop with the Hugo ecosystem. Addresses #9160 and (indirectly) #5792 --- docs/content/en/functions/bool.md | 48 ++++++++++++++++ docs/content/en/functions/truth.md | 53 ++++++++++++++++++ tpl/cast/cast.go | 22 ++++++++ tpl/cast/cast_test.go | 89 ++++++++++++++++++++++++++++++ tpl/cast/init.go | 18 ++++++ 5 files changed, 230 insertions(+) create mode 100644 docs/content/en/functions/bool.md create mode 100644 docs/content/en/functions/truth.md diff --git a/docs/content/en/functions/bool.md b/docs/content/en/functions/bool.md new file mode 100644 index 000000000..535d72c3b --- /dev/null +++ b/docs/content/en/functions/bool.md @@ -0,0 +1,48 @@ +--- +title: bool +linktitle: bool +description: Creates a `bool` from the argument passed into the function. +date: 2023-01-28 +publishdate: 2023-01-28 +lastmod: 2023-01-28 +categories: [functions] +menu: + docs: + parent: "functions" +keywords: [strings,boolean,bool] +signature: ["bool INPUT"] +workson: [] +hugoversion: +relatedfuncs: [truth] +deprecated: false +aliases: [] +--- + +Useful for turning ints, strings, and nil into booleans. + +``` +{{ bool "true" }} → true +{{ bool "false" }} → false + +{{ bool "TRUE" }} → true +{{ bool "FALSE" }} → false + +{{ truth "t" }} → true +{{ truth "f" }} → false + +{{ truth "T" }} → true +{{ truth "F" }} → false + +{{ bool "1" }} → true +{{ bool "0" }} → false + +{{ bool 1 }} → true +{{ bool 0 }} → false + +{{ bool true }} → true +{{ bool false }} → false + +{{ bool nil }} → false +``` + +This function will throw a type-casting error for most other types or strings. For less strict behavior, see [`truth`](/functions/truth). diff --git a/docs/content/en/functions/truth.md b/docs/content/en/functions/truth.md new file mode 100644 index 000000000..88f7e71d1 --- /dev/null +++ b/docs/content/en/functions/truth.md @@ -0,0 +1,53 @@ +--- +title: truth +linktitle: truth +description: Creates a `bool` from the truthyness of the argument passed into the function +date: 2023-01-28 +publishdate: 2023-01-28 +lastmod: 2023-01-28 +categories: [functions] +menu: + docs: + parent: "functions" +keywords: [strings,boolean,bool,truthy,falsey] +signature: ["truth INPUT"] +workson: [] +hugoversion: +relatedfuncs: [bool] +deprecated: false +aliases: [] +--- + +Useful for turning different types into booleans based on their [truthy-ness](https://developer.mozilla.org/en-US/docs/Glossary/Truthy). + +It follows the same rules as [`bool`](/functions/bool), but with increased flexibility. + +``` +{{ truth "true" }} → true +{{ truth "false" }} → false + +{{ truth "TRUE" }} → true +{{ truth "FALSE" }} → false + +{{ truth "t" }} → true +{{ truth "f" }} → false + +{{ truth "T" }} → true +{{ truth "F" }} → false + +{{ truth "1" }} → true +{{ truth "0" }} → false + +{{ truth 1 }} → true +{{ truth 0 }} → false + +{{ truth true }} → true +{{ truth false }} → false + +{{ truth nil }} → false + +{{ truth "cheese" }} → true +{{ truth 1.67 }} → true +``` + +This function will not throw an error. For more strict behavior, see [`bool`](/functions/bool). diff --git a/tpl/cast/cast.go b/tpl/cast/cast.go index 535697f9e..133ab3a47 100644 --- a/tpl/cast/cast.go +++ b/tpl/cast/cast.go @@ -46,6 +46,28 @@ func (ns *Namespace) ToFloat(v any) (float64, error) { return _cast.ToFloat64E(v) } +// ToBool converts v to a boolean. +func (ns *Namespace) ToBool(v any) (bool, error) { + v = convertTemplateToString(v) + return _cast.ToBoolE(v) +} + +// ToTruth yields the same behavior as ToBool when possible. +// If the cast is unsuccessful, ToTruth converts v to a boolean using the JavaScript [definition of truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy). +// Accordingly, it never yields an error, but maintains the signature of other cast methods for consistency. +func (ns *Namespace) ToTruth(v any) (bool, error) { + result, err := ns.ToBool(v) + if err != nil { + switch v { + case "", "nil", "null", "undefined", "NaN": + return false, nil + default: + return true, nil + } + } + return result, nil +} + func convertTemplateToString(v any) any { switch vv := v.(type) { case template.HTML: diff --git a/tpl/cast/cast_test.go b/tpl/cast/cast_test.go index 5b4a36c3a..4c027b956 100644 --- a/tpl/cast/cast_test.go +++ b/tpl/cast/cast_test.go @@ -117,3 +117,92 @@ func TestToFloat(t *testing.T) { c.Assert(result, qt.Equals, test.expect, errMsg) } } + +func TestToBool(t *testing.T) { + t.Parallel() + c := qt.New(t) + ns := New() + + for i, test := range []struct { + v any + expect any + error any + }{ + {"true", true, nil}, + {"false", false, nil}, + {"TRUE", true, nil}, + {"FALSE", false, nil}, + {"t", true, nil}, + {"f", false, nil}, + {"T", true, nil}, + {"F", false, nil}, + {"1", true, nil}, + {"0", false, nil}, + {1, true, nil}, + {0, false, nil}, + {true, true, nil}, + {false, false, nil}, + {nil, false, nil}, + + // error cases + {"cheese", nil, false}, + {"", nil, false}, + {1.67, nil, false}, + } { + errMsg := qt.Commentf("[%d] %v", i, test.v) + + result, err := ns.ToBool(test.v) + + if b, ok := test.error.(bool); ok && !b { + c.Assert(err, qt.Not(qt.IsNil), errMsg) + continue + } + + c.Assert(err, qt.IsNil, errMsg) + c.Assert(result, qt.Equals, test.expect, errMsg) + } +} + +func TestToTruth(t *testing.T) { + t.Parallel() + c := qt.New(t) + ns := New() + + for i, test := range []struct { + v any + expect any + }{ + {"true", true}, + {"false", false}, + {"TRUE", true}, + {"FALSE", false}, + {"t", true}, + {"f", false}, + {"T", true}, + {"F", false}, + {"1", true}, + {"0", false}, + {1, true}, + {0, false}, + {"cheese", true}, + {"", false}, + {1.67, true}, + {template.HTML("2"), true}, + {template.CSS("3"), true}, + {template.HTMLAttr("4"), true}, + {template.JS("-5.67"), true}, + {template.JSStr("6"), true}, + {t, true}, + {nil, false}, + {"null", false}, + {"undefined", false}, + {"NaN", false}, + } { + errMsg := qt.Commentf("[%d] %v", i, test.v) + + result, err := ns.ToTruth(test.v) + + c.Assert(err, qt.IsNil, errMsg) + c.Assert(result, qt.Equals, test.expect, errMsg) + } +} diff --git a/tpl/cast/init.go b/tpl/cast/init.go index 84211a00b..3acc6423b 100644 --- a/tpl/cast/init.go +++ b/tpl/cast/init.go @@ -52,6 +52,24 @@ func init() { }, ) + ns.AddMethodMapping(ctx.ToBool, + []string{"bool"}, + [][2]string{ + {`{{ "0" | bool | printf "%T" }}`, `bool`}, + {`{{ "true" | bool }}`, `true`}, + {`{{ "false" | bool }}`, `false`}, + }, + ) + + ns.AddMethodMapping(ctx.ToTruth, + []string{"truth"}, + [][2]string{ + {`{{ "1234" | truth | printf "%T" }}`, `bool`}, + {`{{ "1234" | truth }}`, `true`}, + {`{{ "" | truth }}`, `false`}, + }, + ) + return ns } From 28434238dbb7245d0ebbcc99b3955f590ffa1c64 Mon Sep 17 00:00:00 2001 From: ksaleem Date: Tue, 17 Oct 2023 23:38:01 -0400 Subject: [PATCH 2/2] tpl: updates cast.ToTruth to conform to hreflect.IsTruthful Concession from review, where cast.ToTruth should behave exactly like hreflect.IsTruthful. Additionally, cast.ToBool always returns a bool with nil error. --- docs/content/en/functions/truth.md | 12 +++++------- tpl/cast/cast.go | 18 +++++++----------- tpl/cast/cast_test.go | 22 ++++++++++------------ 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/docs/content/en/functions/truth.md b/docs/content/en/functions/truth.md index 88f7e71d1..189335df9 100644 --- a/docs/content/en/functions/truth.md +++ b/docs/content/en/functions/truth.md @@ -20,23 +20,21 @@ aliases: [] Useful for turning different types into booleans based on their [truthy-ness](https://developer.mozilla.org/en-US/docs/Glossary/Truthy). -It follows the same rules as [`bool`](/functions/bool), but with increased flexibility. - ``` {{ truth "true" }} → true -{{ truth "false" }} → false +{{ truth "false" }} → true {{ truth "TRUE" }} → true -{{ truth "FALSE" }} → false +{{ truth "FALSE" }} → true {{ truth "t" }} → true -{{ truth "f" }} → false +{{ truth "f" }} → true {{ truth "T" }} → true -{{ truth "F" }} → false +{{ truth "F" }} → true {{ truth "1" }} → true -{{ truth "0" }} → false +{{ truth "0" }} → true {{ truth 1 }} → true {{ truth 0 }} → false diff --git a/tpl/cast/cast.go b/tpl/cast/cast.go index 133ab3a47..d0e291dbf 100644 --- a/tpl/cast/cast.go +++ b/tpl/cast/cast.go @@ -17,6 +17,7 @@ package cast import ( "html/template" + "github.com/gohugoio/hugo/common/hreflect" _cast "github.com/spf13/cast" ) @@ -49,23 +50,18 @@ func (ns *Namespace) ToFloat(v any) (float64, error) { // ToBool converts v to a boolean. func (ns *Namespace) ToBool(v any) (bool, error) { v = convertTemplateToString(v) - return _cast.ToBoolE(v) + result, err := _cast.ToBoolE(v) + if err != nil { + return false, nil + } + return result, nil } // ToTruth yields the same behavior as ToBool when possible. // If the cast is unsuccessful, ToTruth converts v to a boolean using the JavaScript [definition of truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy). // Accordingly, it never yields an error, but maintains the signature of other cast methods for consistency. func (ns *Namespace) ToTruth(v any) (bool, error) { - result, err := ns.ToBool(v) - if err != nil { - switch v { - case "", "nil", "null", "undefined", "NaN": - return false, nil - default: - return true, nil - } - } - return result, nil + return hreflect.IsTruthful(v), nil } func convertTemplateToString(v any) any { diff --git a/tpl/cast/cast_test.go b/tpl/cast/cast_test.go index 4c027b956..5f2f50241 100644 --- a/tpl/cast/cast_test.go +++ b/tpl/cast/cast_test.go @@ -144,10 +144,8 @@ func TestToBool(t *testing.T) { {false, false, nil}, {nil, false, nil}, - // error cases - {"cheese", nil, false}, - {"", nil, false}, - {1.67, nil, false}, + {"cheese", false, nil}, + {"", false, nil}, } { errMsg := qt.Commentf("[%d] %v", i, test.v) @@ -173,15 +171,15 @@ func TestToTruth(t *testing.T) { expect any }{ {"true", true}, - {"false", false}, + {"false", true}, {"TRUE", true}, - {"FALSE", false}, + {"FALSE", true}, {"t", true}, - {"f", false}, + {"f", true}, {"T", true}, - {"F", false}, + {"F", true}, {"1", true}, - {"0", false}, + {"0", true}, {1, true}, {0, false}, {"cheese", true}, @@ -194,9 +192,9 @@ func TestToTruth(t *testing.T) { {template.JSStr("6"), true}, {t, true}, {nil, false}, - {"null", false}, - {"undefined", false}, - {"NaN", false}, + {"null", true}, + {"undefined", true}, + {"NaN", true}, } { errMsg := qt.Commentf("[%d] %v", i, test.v)