diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go new file mode 100644 index 00000000000..7d62481381f --- /dev/null +++ b/common/hreflect/helpers.go @@ -0,0 +1,110 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package hreflect contains reflect helpers. +package hreflect + +import "reflect" + +// Indirect returns the item at the end of indirection, and a bool to indicate +// if it's nil. If the returned bool is true, the returned value's kind will be +// either a pointer or interface. +// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L918 +func Indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} + +// indirectInterface returns the concrete value in an interface value, +// or else the zero reflect.Value, and a boolean indicating if it is nil. +// That is, if v represents the interface value x, the result is the same as reflect.ValueOf(x): +// the fact that x was an interface value is forgotten. +// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931 +func IndirectInterface(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} + +func IsTruthful(in interface{}) bool { + switch v := in.(type) { + case reflect.Value: + return IsTruthfulValue(v) + default: + return IsTruthfulValue(reflect.ValueOf(in)) + } + +} + +type zeroer interface { + IsZero() bool +} + +var zeroType = reflect.TypeOf((*zeroer)(nil)).Elem() + +// IsTruthful returns whether the given value has a meaningful truth value. +// This is based on template.IsTrue in Go's stdlib, but also considers +// IsZero and any interface value will be unwrapped before it's considered +// for truthfulness. +// +// Based on: +// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306 +func IsTruthfulValue(val reflect.Value) (truth bool) { + if !val.IsValid() { + // Something like var x interface{}, never set. It's a form of nil. + return false + } + + val, _ = IndirectInterface(val) + + if val.Type().Implements(zeroType) { + return !val.Interface().(zeroer).IsZero() + } + + switch val.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + truth = val.Len() > 0 + case reflect.Bool: + truth = val.Bool() + case reflect.Complex64, reflect.Complex128: + truth = val.Complex() != 0 + case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface: + truth = !val.IsNil() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + truth = val.Int() != 0 + case reflect.Float32, reflect.Float64: + truth = val.Float() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + truth = val.Uint() != 0 + case reflect.Struct: + truth = true // Struct values are always true. + default: + return + } + return truth +} diff --git a/common/hreflect/helpers_test.go b/common/hreflect/helpers_test.go new file mode 100644 index 00000000000..46527e7645c --- /dev/null +++ b/common/hreflect/helpers_test.go @@ -0,0 +1,30 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hreflect + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestIsTruthFul(t *testing.T) { + assert := require.New(t) + + assert.True(IsTruthful(true)) + assert.False(IsTruthful(false)) + assert.True(IsTruthful(time.Now())) + assert.False(IsTruthful(time.Time{})) +} diff --git a/go.sum b/go.sum index 79d7c1eb0b8..0227e3b9cc9 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,7 @@ github.com/magefile/mage v1.4.0 h1:RI7B1CgnPAuu2O9lWszwya61RLmfL0KCdo+QyyI/Bhk= github.com/magefile/mage v1.4.0/go.mod h1:IUDi13rsHje59lecXokTfGX0QIzO45uVPlXnJYsXepA= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/markbates/inflect v0.0.0-20171215194931-a12c3aec81a6 h1:LZhVjIISSbj8qLf2qDPP0D8z0uvOWAW5C85ly5mJW6c= github.com/markbates/inflect v0.0.0-20171215194931-a12c3aec81a6/go.mod h1:oTeZL2KHA7CUX6X+fovmK9OvIOFuqu0TwdQrZjLTh88= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= diff --git a/tpl/collections/apply.go b/tpl/collections/apply.go index d715aeb007d..b3b19d9db2e 100644 --- a/tpl/collections/apply.go +++ b/tpl/collections/apply.go @@ -1,4 +1,4 @@ -// Copyright 2017 The Hugo Authors. All rights reserved. +// Copyright 2019 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import ( "reflect" "strings" + "github.com/gohugoio/hugo/common/hreflect" + "github.com/gohugoio/hugo/tpl" ) @@ -33,7 +35,7 @@ func (ns *Namespace) Apply(seq interface{}, fname string, args ...interface{}) ( } seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) + seqv, isNil := hreflect.Indirect(seqv) if isNil { return nil, errors.New("can't iterate over a nil value") } @@ -135,28 +137,3 @@ func (ns *Namespace) lookupFunc(fname string) (reflect.Value, bool) { } return m, true } - -// indirect is borrowed from the Go stdlib: 'text/template/exec.go' -func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { - for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { - if v.IsNil() { - return v, true - } - if v.Kind() == reflect.Interface && v.NumMethod() > 0 { - break - } - } - return v, false -} - -func indirectInterface(v reflect.Value) (rv reflect.Value, isNil bool) { - for ; v.Kind() == reflect.Interface; v = v.Elem() { - if v.IsNil() { - return v, true - } - if v.Kind() == reflect.Interface && v.NumMethod() > 0 { - break - } - } - return v, false -} diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go index bad65369fab..1c8aa796cd5 100644 --- a/tpl/collections/collections.go +++ b/tpl/collections/collections.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. +// Copyright 2019 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import ( "time" "github.com/gohugoio/hugo/common/collections" + "github.com/gohugoio/hugo/common/hreflect" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/deps" @@ -65,7 +66,7 @@ func (ns *Namespace) After(index interface{}, seq interface{}) (interface{}, err } seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) + seqv, isNil := hreflect.Indirect(seqv) if isNil { return nil, errors.New("can't iterate over a nil value") } @@ -103,7 +104,7 @@ func (ns *Namespace) Delimit(seq, delimiter interface{}, last ...interface{}) (t } seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) + seqv, isNil := hreflect.Indirect(seqv) if isNil { return "", errors.New("can't iterate over a nil value") } @@ -165,7 +166,7 @@ func (ns *Namespace) Dictionary(values ...interface{}) (map[string]interface{}, // EchoParam returns a given value if it is set; otherwise, it returns an // empty string. func (ns *Namespace) EchoParam(a, key interface{}) interface{} { - av, isNil := indirect(reflect.ValueOf(a)) + av, isNil := hreflect.Indirect(reflect.ValueOf(a)) if isNil { return "" } @@ -184,7 +185,7 @@ func (ns *Namespace) EchoParam(a, key interface{}) interface{} { } } - avv, isNil = indirect(avv) + avv, isNil = hreflect.Indirect(avv) if isNil { return "" @@ -222,7 +223,7 @@ func (ns *Namespace) First(limit interface{}, seq interface{}) (interface{}, err } seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) + seqv, isNil := hreflect.Indirect(seqv) if isNil { return nil, errors.New("can't iterate over a nil value") } @@ -254,7 +255,7 @@ func (ns *Namespace) In(l interface{}, v interface{}) bool { case reflect.Array, reflect.Slice: for i := 0; i < lv.Len(); i++ { lvv := lv.Index(i) - lvv, isNil := indirect(lvv) + lvv, isNil := hreflect.Indirect(lvv) if isNil { continue } @@ -380,7 +381,7 @@ func (ns *Namespace) Last(limit interface{}, seq interface{}) (interface{}, erro } seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) + seqv, isNil := hreflect.Indirect(seqv) if isNil { return nil, errors.New("can't iterate over a nil value") } @@ -496,7 +497,7 @@ func (ns *Namespace) Shuffle(seq interface{}) (interface{}, error) { } seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) + seqv, isNil := hreflect.Indirect(seqv) if isNil { return nil, errors.New("can't iterate over a nil value") } @@ -600,7 +601,7 @@ func (ns *Namespace) Union(l1, l2 interface{}) (interface{}, error) { ) for i := 0; i < l1v.Len(); i++ { - l1vv, isNil = indirectInterface(l1v.Index(i)) + l1vv, isNil = hreflect.IndirectInterface(l1v.Index(i)) if !l1vv.Type().Comparable() { return []interface{}{}, errors.New("union does not support slices or arrays of uncomparable types") @@ -657,7 +658,7 @@ func (ns *Namespace) Uniq(l interface{}) (interface{}, error) { } lv := reflect.ValueOf(l) - lv, isNil := indirect(lv) + lv, isNil := hreflect.Indirect(lv) if isNil { return nil, errors.New("invalid nil argument to Uniq") } @@ -675,7 +676,7 @@ func (ns *Namespace) Uniq(l interface{}) (interface{}, error) { for i := 0; i != lv.Len(); i++ { lvv := lv.Index(i) - lvv, isNil := indirect(lvv) + lvv, isNil := hreflect.Indirect(lvv) if isNil { continue } diff --git a/tpl/collections/complement.go b/tpl/collections/complement.go index a5633f8b422..fc3351fb5e5 100644 --- a/tpl/collections/complement.go +++ b/tpl/collections/complement.go @@ -17,6 +17,8 @@ import ( "errors" "fmt" "reflect" + + "github.com/gohugoio/hugo/common/hreflect" ) // Complement gives the elements in the last element of seqs that are not in @@ -43,7 +45,7 @@ func (ns *Namespace) Complement(seqs ...interface{}) (interface{}, error) { case reflect.Array, reflect.Slice: sl := reflect.MakeSlice(v.Type(), 0, 0) for i := 0; i < v.Len(); i++ { - ev, _ := indirectInterface(v.Index(i)) + ev, _ := hreflect.IndirectInterface(v.Index(i)) if !ev.Type().Comparable() { return nil, errors.New("elements in complement must be comparable") } diff --git a/tpl/collections/index.go b/tpl/collections/index.go index b081511885e..375f2d289bb 100644 --- a/tpl/collections/index.go +++ b/tpl/collections/index.go @@ -17,6 +17,8 @@ import ( "errors" "fmt" "reflect" + + "github.com/gohugoio/hugo/common/hreflect" ) // Index returns the result of indexing its first argument by the following @@ -36,7 +38,7 @@ func (ns *Namespace) Index(item interface{}, indices ...interface{}) (interface{ for _, i := range indices { index := reflect.ValueOf(i) var isNil bool - if v, isNil = indirect(v); isNil { + if v, isNil = hreflect.Indirect(v); isNil { return nil, errors.New("index of nil pointer") } switch v.Kind() { diff --git a/tpl/collections/reflect_helpers.go b/tpl/collections/reflect_helpers.go index fca65481f30..0b207e3ae03 100644 --- a/tpl/collections/reflect_helpers.go +++ b/tpl/collections/reflect_helpers.go @@ -18,6 +18,7 @@ import ( "reflect" "time" + "github.com/gohugoio/hugo/common/hreflect" "github.com/pkg/errors" ) @@ -66,7 +67,7 @@ func collectIdentities(seqs ...interface{}) (map[interface{}]bool, error) { switch v.Kind() { case reflect.Array, reflect.Slice: for i := 0; i < v.Len(); i++ { - ev, _ := indirectInterface(v.Index(i)) + ev, _ := hreflect.IndirectInterface(v.Index(i)) if !ev.Type().Comparable() { return nil, errors.New("elements must be comparable") diff --git a/tpl/collections/sort.go b/tpl/collections/sort.go index 206a19cb5ea..799f13d7185 100644 --- a/tpl/collections/sort.go +++ b/tpl/collections/sort.go @@ -15,6 +15,9 @@ package collections import ( "errors" + + "github.com/gohugoio/hugo/common/hreflect" + "reflect" "sort" "strings" @@ -32,7 +35,7 @@ func (ns *Namespace) Sort(seq interface{}, args ...interface{}) (interface{}, er } seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) + seqv, isNil := hreflect.Indirect(seqv) if isNil { return nil, errors.New("can't iterate over a nil value") } diff --git a/tpl/collections/symdiff.go b/tpl/collections/symdiff.go index 1c58257e4eb..e6a6a1c88d9 100644 --- a/tpl/collections/symdiff.go +++ b/tpl/collections/symdiff.go @@ -17,6 +17,8 @@ import ( "fmt" "reflect" + "github.com/gohugoio/hugo/common/hreflect" + "github.com/pkg/errors" ) @@ -47,7 +49,7 @@ func (ns *Namespace) SymDiff(s2, s1 interface{}) (interface{}, error) { } for i := 0; i < v.Len(); i++ { - ev, _ := indirectInterface(v.Index(i)) + ev, _ := hreflect.IndirectInterface(v.Index(i)) if !ev.Type().Comparable() { return nil, errors.New("symdiff: elements must be comparable") } diff --git a/tpl/collections/where.go b/tpl/collections/where.go index 2c5dc7f3fac..43c39750297 100644 --- a/tpl/collections/where.go +++ b/tpl/collections/where.go @@ -14,6 +14,8 @@ package collections import ( + "github.com/gohugoio/hugo/common/hreflect" + "errors" "fmt" "reflect" @@ -22,7 +24,7 @@ import ( // Where returns a filtered subset of a given data type. func (ns *Namespace) Where(seq, key interface{}, args ...interface{}) (interface{}, error) { - seqv, isNil := indirect(reflect.ValueOf(seq)) + seqv, isNil := hreflect.Indirect(reflect.ValueOf(seq)) if isNil { return nil, errors.New("can't iterate over a nil value of type " + reflect.ValueOf(seq).Type().String()) } @@ -49,12 +51,12 @@ func (ns *Namespace) Where(seq, key interface{}, args ...interface{}) (interface } func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error) { - v, vIsNil := indirect(v) + v, vIsNil := hreflect.Indirect(v) if !v.IsValid() { vIsNil = true } - mv, mvIsNil := indirect(mv) + mv, mvIsNil := hreflect.Indirect(mv) if !mv.IsValid() { mvIsNil = true } @@ -267,7 +269,7 @@ func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) return zero, errors.New("can't evaluate an invalid value") } typ := obj.Type() - obj, isNil := indirect(obj) + obj, isNil := hreflect.Indirect(obj) // first, check whether obj has a method. In this case, obj is // an interface, a struct or its pointer. If obj is a struct, @@ -361,7 +363,7 @@ func (ns *Namespace) checkWhereArray(seqv, kv, mv reflect.Value, path []string, } } } else { - vv, _ := indirect(rvv) + vv, _ := hreflect.Indirect(rvv) if vv.Kind() == reflect.Map && kv.Type().AssignableTo(vv.Type().Key()) { vvv = vv.MapIndex(kv) } @@ -396,7 +398,7 @@ func (ns *Namespace) checkWhereMap(seqv, kv, mv reflect.Value, path []string, op } } case reflect.Interface: - elemvv, isNil := indirect(elemv) + elemvv, isNil := hreflect.Indirect(elemv) if isNil { continue } diff --git a/tpl/compare/init.go b/tpl/compare/init.go index f766ef890f9..0dac1131217 100644 --- a/tpl/compare/init.go +++ b/tpl/compare/init.go @@ -71,6 +71,26 @@ func init() { [][2]string{}, ) + ns.AddMethodMapping(ctx.And, + []string{"and"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Or, + []string{"or"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.IsTrue, + []string{"istrue", "isTrue"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Not, + []string{"not"}, + [][2]string{}, + ) + ns.AddMethodMapping(ctx.Conditional, []string{"cond"}, [][2]string{ diff --git a/tpl/compare/truth.go b/tpl/compare/truth.go new file mode 100644 index 00000000000..786bca86ae5 --- /dev/null +++ b/tpl/compare/truth.go @@ -0,0 +1,72 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// The functions in this file is based on the Go source code, copyright +// The Go Authors and governed by a BSD-style license. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package compare provides template functions for comparing values. +package compare + +import ( + "reflect" + + "github.com/gohugoio/hugo/common/hreflect" +) + +// Boolean logic, based on: +// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/funcs.go#L302 + +func truth(arg reflect.Value) bool { + return hreflect.IsTruthfulValue(arg) +} + +// IsTrue returns whether the given value has a meaningful truth value, i.e. +// a value that is not nil and not zero of it's value. +// This is what's used by if, with, and, or and not. +func (*Namespace) IsTrue(arg reflect.Value) bool { + return truth(arg) +} + +// And computes the Boolean AND of its arguments, returning +// the first false argument it encounters, or the last argument. +func (*Namespace) And(arg0 reflect.Value, args ...reflect.Value) reflect.Value { + if !truth(arg0) { + return arg0 + } + for i := range args { + arg0 = args[i] + if !truth(arg0) { + break + } + } + return arg0 +} + +// Or computes the Boolean OR of its arguments, returning +// the first true argument it encounters, or the last argument. +func (*Namespace) Or(arg0 reflect.Value, args ...reflect.Value) reflect.Value { + if truth(arg0) { + return arg0 + } + for i := range args { + arg0 = args[i] + if truth(arg0) { + break + } + } + return arg0 +} + +// Not returns the Boolean negation of its argument. +func (*Namespace) Not(arg reflect.Value) bool { + return !truth(arg) +} diff --git a/tpl/compare/truth_test.go b/tpl/compare/truth_test.go new file mode 100644 index 00000000000..4d789475646 --- /dev/null +++ b/tpl/compare/truth_test.go @@ -0,0 +1,59 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "reflect" + "testing" + "time" + + "github.com/gohugoio/hugo/common/hreflect" + "github.com/stretchr/testify/require" +) + +func TestTruth(t *testing.T) { + n := New() + + truthv, falsev := reflect.ValueOf(time.Now()), reflect.ValueOf(false) + + assertTruth := func(t *testing.T, v reflect.Value, expected bool) { + if hreflect.IsTruthfulValue(v) != expected { + t.Fatal("truth mismatch") + } + } + + t.Run("And", func(t *testing.T) { + assertTruth(t, n.And(truthv, truthv), true) + assertTruth(t, n.And(truthv, falsev), false) + + }) + + t.Run("Or", func(t *testing.T) { + assertTruth(t, n.Or(truthv, truthv), true) + assertTruth(t, n.Or(falsev, truthv, falsev), true) + assertTruth(t, n.Or(falsev, falsev), false) + }) + + t.Run("Not", func(t *testing.T) { + assert := require.New(t) + assert.True(n.Not(falsev)) + assert.False(n.Not(truthv)) + }) + + t.Run("IsTrue", func(t *testing.T) { + assert := require.New(t) + assert.False(n.IsTrue(falsev)) + assert.True(n.IsTrue(truthv)) + }) +} diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go index f32b189ffbc..b17596342e4 100644 --- a/tpl/tplimpl/template_ast_transformers.go +++ b/tpl/tplimpl/template_ast_transformers.go @@ -90,6 +90,31 @@ func applyTemplateTransformers(templ *parse.Tree, lookupFn func(name string) *pa return nil } +// The truth logic in Go's template package is broken for certain values +// for the if and with keywords. This works around that problem by wrapping +// the node passed to if/with in a an istrue condtitional. +// istrue works slightly different than the Gon built-in in that it also +// considers any IsZero methods on the values (as in time.Time). +// See https://github.com/gohugoio/hugo/issues/5738 +func (c *templateContext) wrapWithIsTrue(p *parse.PipeNode) { + if len(p.Cmds) == 0 { + return + } + + firstArg := parse.NewIdentifier("istrue") + secondArg := p.CopyPipe() + newCmd := p.Cmds[0].Copy().(*parse.CommandNode) + + // secondArg is a PipeNode and will behave as it was wrapped in parens, e.g: + // {{ istrue (len .Params | eq 2) }} + newCmd.Args = []parse.Node{firstArg, secondArg} + + p.Cmds = []*parse.CommandNode{newCmd} + + //fmt.Println("P:", p, ">>") + +} + // paramsKeysToLower is made purposely non-generic to make it not so tempting // to do more of these hard-to-maintain AST transformations. func (c *templateContext) paramsKeysToLower(n parse.Node) { @@ -102,8 +127,10 @@ func (c *templateContext) paramsKeysToLower(n parse.Node) { c.paramsKeysToLowerForNodes(x.Pipe) case *parse.IfNode: c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) + c.wrapWithIsTrue(x.Pipe) case *parse.WithNode: c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) + c.wrapWithIsTrue(x.Pipe) case *parse.RangeNode: c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) case *parse.TemplateNode: diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go index 45cf4399a9b..ada8bd53921 100644 --- a/tpl/tplimpl/template_ast_transformers_test.go +++ b/tpl/tplimpl/template_ast_transformers_test.go @@ -15,15 +15,25 @@ package tplimpl import ( "bytes" "fmt" + "html/template" "testing" + "time" - "html/template" + "github.com/gohugoio/hugo/tpl" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/common/hreflect" "github.com/spf13/cast" "github.com/stretchr/testify/require" ) +type zeroer interface { + IsZero() bool +} + var ( testFuncs = map[string]interface{}{ "ToTime": func(v interface{}) interface{} { return cast.ToTime(v) }, @@ -41,6 +51,9 @@ var ( }, } }, + "istrue": func(v interface{}) bool { + return hreflect.IsTruthful(v) + }, } paramsData = map[string]interface{}{ @@ -380,3 +393,65 @@ func TestTransformRecursiveTemplate(t *testing.T) { c.paramsKeysToLower(templ.Tree.Root) } + +func TestInsertIsZeroFunc(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + var ( + ctx = map[string]interface{}{ + "Params": map[string]interface{}{ + "colors": map[string]interface{}{ + "blue": "Amber", + "pretty": map[string]interface{}{ + "first": "Indigo", + }, + }, + }, + "True": true, + "Now": time.Now(), + "TimeZero": time.Time{}, + } + + templ = ` +{{ if .True }}.True: TRUE{{ else }}.True: FALSE{{ end }} +{{ if .TimeZero }}.TimeZero1: TRUE{{ else }}.TimeZero1: FALSE{{ end }} +{{ if (.TimeZero) }}.TimeZero2: TRUE{{ else }}.TimeZero2: FALSE{{ end }} +{{ if not .TimeZero }}.TimeZero3: TRUE{{ else }}.TimeZero3: FALSE{{ end }} + +{{ if .Now }}.Now: TRUE{{ else }}.Now: FALSE{{ end }} + +{{ with .TimeZero }}.TimeZero1 with: {{ . }}{{ else }}.TimeZero1 with: FALSE{{ end }} + + +` + ) + + v := newTestConfig() + fs := hugofs.NewMem(v) + + depsCfg := newDepsConfig(v) + depsCfg.Fs = fs + d, err := deps.New(depsCfg) + assert.NoError(err) + + provider := DefaultTemplateProvider + provider.Update(d) + + h := d.Tmpl.(handler) + + assert.NoError(h.addTemplate("mytemplate.html", templ)) + + tt, _ := d.Tmpl.Lookup("mytemplate.html") + result, err := tt.(tpl.TemplateExecutor).ExecuteToString(ctx) + assert.NoError(err) + + assert.Contains(result, ".True: TRUE") + assert.Contains(result, ".TimeZero1: FALSE") + assert.Contains(result, ".TimeZero2: FALSE") + assert.Contains(result, ".TimeZero3: TRUE") + assert.Contains(result, ".Now: TRUE") + assert.Contains(result, "TimeZero1 with: FALSE") + +}