From 5a72de2600c1dfa0ea2d16856e7531a64102b576 Mon Sep 17 00:00:00 2001 From: Khayyam Saleem Date: Sat, 28 Jan 2023 12:21:22 -0500 Subject: [PATCH] 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 }