From 4d7c62a5f45f4404e6dd54da3b5317b91cd5c6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 6 Jun 2022 09:48:40 +0200 Subject: [PATCH] Fix raw TOML dates in where/eq Note that this has only been a problem with "raw dates" in TOML files in /data and similar. The predefined front matter dates `.Date` etc. are converted to a Go Time and has worked fine even after upgrading to v2 of the go-toml lib. Fixes #9979 --- common/hreflect/helpers.go | 39 +++++++++++++++++++++++ common/htime/time.go | 5 +++ hugolib/dates_test.go | 50 ++++++++++++++++++++++++++++++ tpl/collections/collections.go | 16 ++++++++-- tpl/collections/reflect_helpers.go | 2 -- tpl/collections/sort.go | 11 +++---- tpl/collections/where.go | 24 ++++++-------- tpl/compare/compare.go | 34 ++++++++++---------- tpl/compare/init.go | 7 ++++- 9 files changed, 147 insertions(+), 41 deletions(-) diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go index e1c01456d4f..89451ade77b 100644 --- a/common/hreflect/helpers.go +++ b/common/hreflect/helpers.go @@ -20,7 +20,9 @@ import ( "context" "reflect" "sync" + "time" + "github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/types" ) @@ -168,6 +170,43 @@ func GetMethodIndexByName(tp reflect.Type, name string) int { return m.Index } +var ( + timeType = reflect.TypeOf((*time.Time)(nil)).Elem() + asTimeProviderType = reflect.TypeOf((*htime.AsTimeProvider)(nil)).Elem() +) + +// IsTime returns whether tp is a time.Time type or if it can be converted into one +// in ToTime. +func IsTime(tp reflect.Type) bool { + if tp == timeType { + return true + } + + if tp.Implements(asTimeProviderType) { + return true + } + return false +} + +// ToTimeE tries to convert the given value to a time.Time. +// A zero Time and false is returned if this isn't possible. +// Note that this function does not accept string dates etc. +func ToTime(v reflect.Value, loc *time.Location) (time.Time, bool) { + if v.Kind() == reflect.Interface { + return ToTime(v.Elem(), loc) + } + + if v.Type() == timeType { + return v.Interface().(time.Time), true + } + + if v.Type().Implements(asTimeProviderType) { + return v.Interface().(htime.AsTimeProvider).AsTime(loc), true + + } + return time.Time{}, false +} + // Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931 func indirectInterface(v reflect.Value) reflect.Value { if v.Kind() != reflect.Interface { diff --git a/common/htime/time.go b/common/htime/time.go index 052a45ed1dd..a5ceb68a70f 100644 --- a/common/htime/time.go +++ b/common/htime/time.go @@ -161,3 +161,8 @@ func Now() time.Time { func Since(t time.Time) time.Duration { return Clock.Since(t) } + +// AsTimeProvider is implemented by go-toml's LocalDate and LocalDateTime. +type AsTimeProvider interface { + AsTime(zone *time.Location) time.Time +} diff --git a/hugolib/dates_test.go b/hugolib/dates_test.go index 4b4dc29d209..cc795da1878 100644 --- a/hugolib/dates_test.go +++ b/hugolib/dates_test.go @@ -214,3 +214,53 @@ func TestTimeOnError(t *testing.T) { b.Assert(b.BuildE(BuildCfg{}), qt.Not(qt.IsNil)) } + +func TestTOMLDates(t *testing.T) { + t.Parallel() + + files := ` +-- config.toml -- +-- content/_index.md -- +--- +date: "2020-10-20" +--- + +## Foo +-- data/mydata.toml -- +date = 2020-10-20 +talks = [ + { date = 2017-01-23, name = "Past talk 1" }, + { date = 2017-01-24, name = "Past talk 2" }, + { date = 2017-01-26, name = "Past talk 3" }, + { date = 2050-02-12, name = "Future talk 1" }, + { date = 2050-02-13, name = "Future talk 2" }, +] +-- layouts/index.html -- +{{ $futureTalks := where site.Data.mydata.talks "date" ">" now }} +{{ $pastTalks := where site.Data.mydata.talks "date" "<" now }} + +{{ $homeDate := site.Home.Date }} +Future talks: {{ len $futureTalks }} +Past talks: {{ len $pastTalks }} + +Home's Date should be greater than past: {{ gt $homeDate (index $pastTalks 0).date }} +Home's Date should be less than future: {{ lt $homeDate (index $futureTalks 0).date }} +Home's Date should be equal mydata date: {{ eq $homeDate site.Data.mydata.date }} + +` + + b := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).Build() + + b.AssertFileContent("public/index.html", ` +Future talks: 2 +Past talks: 3 +Home's Date should be greater than past: true +Home's Date should be less than future: true +Home's Date should be equal mydata date: true +`) +} diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go index 8fdc8527586..299a504f415 100644 --- a/tpl/collections/collections.go +++ b/tpl/collections/collections.go @@ -31,6 +31,8 @@ import ( "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/langs" + "github.com/gohugoio/hugo/tpl/compare" "github.com/spf13/cast" ) @@ -41,14 +43,24 @@ func init() { // New returns a new instance of the collections-namespaced template functions. func New(deps *deps.Deps) *Namespace { + if deps.Language == nil { + panic("language must be set") + } + + loc := langs.GetLocation(deps.Language) + return &Namespace{ - deps: deps, + loc: loc, + sortComp: compare.New(loc, true), + deps: deps, } } // Namespace provides template functions for the "collections" namespace. type Namespace struct { - deps *deps.Deps + loc *time.Location + sortComp *compare.Namespace + deps *deps.Deps } // After returns all the items after the first N in a rangeable list. diff --git a/tpl/collections/reflect_helpers.go b/tpl/collections/reflect_helpers.go index 4178850aa35..a295441ecdb 100644 --- a/tpl/collections/reflect_helpers.go +++ b/tpl/collections/reflect_helpers.go @@ -16,7 +16,6 @@ package collections import ( "fmt" "reflect" - "time" "errors" @@ -26,7 +25,6 @@ import ( var ( zero reflect.Value errorType = reflect.TypeOf((*error)(nil)).Elem() - timeType = reflect.TypeOf((*time.Time)(nil)).Elem() ) func numberToFloat(v reflect.Value) (float64, error) { diff --git a/tpl/collections/sort.go b/tpl/collections/sort.go index a0c2f815b19..ce76a4522ae 100644 --- a/tpl/collections/sort.go +++ b/tpl/collections/sort.go @@ -25,8 +25,6 @@ import ( "github.com/spf13/cast" ) -var sortComp = compare.New(true) - // Sort returns a sorted sequence. func (ns *Namespace) Sort(seq any, args ...any) (any, error) { if seq == nil { @@ -51,7 +49,7 @@ func (ns *Namespace) Sort(seq any, args ...any) (any, error) { collator := langs.GetCollator(ns.deps.Language) // Create a list of pairs that will be used to do the sort - p := pairList{Collator: collator, SortAsc: true, SliceType: sliceType} + p := pairList{Collator: collator, sortComp: ns.sortComp, SortAsc: true, SliceType: sliceType} p.Pairs = make([]pair, seqv.Len()) var sortByField string @@ -145,6 +143,7 @@ type pair struct { // A slice of pairs that implements sort.Interface to sort by Value. type pairList struct { Collator *langs.Collator + sortComp *compare.Namespace Pairs []pair SortAsc bool SliceType reflect.Type @@ -159,16 +158,16 @@ func (p pairList) Less(i, j int) bool { if iv.IsValid() { if jv.IsValid() { // can only call Interface() on valid reflect Values - return sortComp.LtCollate(p.Collator, iv.Interface(), jv.Interface()) + return p.sortComp.LtCollate(p.Collator, iv.Interface(), jv.Interface()) } // if j is invalid, test i against i's zero value - return sortComp.LtCollate(p.Collator, iv.Interface(), reflect.Zero(iv.Type())) + return p.sortComp.LtCollate(p.Collator, iv.Interface(), reflect.Zero(iv.Type())) } if jv.IsValid() { // if i is invalid, test j against j's zero value - return sortComp.LtCollate(p.Collator, reflect.Zero(jv.Type()), jv.Interface()) + return p.sortComp.LtCollate(p.Collator, reflect.Zero(jv.Type()), jv.Interface()) } return false diff --git a/tpl/collections/where.go b/tpl/collections/where.go index 8a9df445d02..f861e91ab0d 100644 --- a/tpl/collections/where.go +++ b/tpl/collections/where.go @@ -107,11 +107,10 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error fmv := mv.Float() fmvp = &fmv case reflect.Struct: - switch v.Type() { - case timeType: - iv := toTimeUnix(v) + if hreflect.IsTime(v.Type()) { + iv := ns.toTimeUnix(v) ivp = &iv - imv := toTimeUnix(mv) + imv := ns.toTimeUnix(mv) imvp = &imv } case reflect.Array, reflect.Slice: @@ -167,12 +166,11 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error } } case reflect.Struct: - switch v.Type() { - case timeType: - iv := toTimeUnix(v) + if hreflect.IsTime(v.Type()) { + iv := ns.toTimeUnix(v) ivp = &iv for i := 0; i < mv.Len(); i++ { - ima = append(ima, toTimeUnix(mv.Index(i))) + ima = append(ima, ns.toTimeUnix(mv.Index(i))) } } case reflect.Array, reflect.Slice: @@ -508,12 +506,10 @@ func toString(v reflect.Value) (string, error) { return "", errors.New("unable to convert value to string") } -func toTimeUnix(v reflect.Value) int64 { - if v.Kind() == reflect.Interface { - return toTimeUnix(v.Elem()) - } - if v.Type() != timeType { +func (ns *Namespace) toTimeUnix(v reflect.Value) int64 { + t, ok := hreflect.ToTime(v, ns.loc) + if !ok { panic("coding error: argument must be time.Time type reflect Value") } - return hreflect.GetMethodByName(v, "Unix").Call([]reflect.Value{})[0].Int() + return t.Unix() } diff --git a/tpl/compare/compare.go b/tpl/compare/compare.go index 9905003b23c..ee8111ad0c6 100644 --- a/tpl/compare/compare.go +++ b/tpl/compare/compare.go @@ -23,16 +23,19 @@ import ( "github.com/gohugoio/hugo/compare" "github.com/gohugoio/hugo/langs" + "github.com/gohugoio/hugo/common/hreflect" + "github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/types" ) // New returns a new instance of the compare-namespaced template functions. -func New(caseInsensitive bool) *Namespace { - return &Namespace{caseInsensitive: caseInsensitive} +func New(loc *time.Location, caseInsensitive bool) *Namespace { + return &Namespace{loc: loc, caseInsensitive: caseInsensitive} } // Namespace provides template functions for the "compare" namespace. type Namespace struct { + loc *time.Location // Enable to do case insensitive string compares. caseInsensitive bool } @@ -101,6 +104,11 @@ func (n *Namespace) Eq(first any, others ...any) bool { if types.IsNil(v) { return nil } + + if at, ok := v.(htime.AsTimeProvider); ok { + return at.AsTime(n.loc) + } + vv := reflect.ValueOf(v) switch vv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: @@ -269,9 +277,8 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a leftStr = &str } case reflect.Struct: - switch av.Type() { - case timeType: - left = float64(toTimeUnix(av)) + if hreflect.IsTime(av.Type()) { + left = float64(ns.toTimeUnix(av)) } case reflect.Bool: left = 0 @@ -297,9 +304,8 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a rightStr = &str } case reflect.Struct: - switch bv.Type() { - case timeType: - right = float64(toTimeUnix(bv)) + if hreflect.IsTime(bv.Type()) { + right = float64(ns.toTimeUnix(bv)) } case reflect.Bool: right = 0 @@ -337,14 +343,10 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a return left, right } -var timeType = reflect.TypeOf((*time.Time)(nil)).Elem() - -func toTimeUnix(v reflect.Value) int64 { - if v.Kind() == reflect.Interface { - return toTimeUnix(v.Elem()) - } - if v.Type() != timeType { +func (ns *Namespace) toTimeUnix(v reflect.Value) int64 { + t, ok := hreflect.ToTime(v, ns.loc) + if !ok { panic("coding error: argument must be time.Time type reflect Value") } - return v.MethodByName("Unix").Call([]reflect.Value{})[0].Int() + return t.Unix() } diff --git a/tpl/compare/init.go b/tpl/compare/init.go index 2308b235e77..98c07f41b8d 100644 --- a/tpl/compare/init.go +++ b/tpl/compare/init.go @@ -15,6 +15,7 @@ package compare import ( "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/tpl/internal" ) @@ -22,7 +23,11 @@ const name = "compare" func init() { f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { - ctx := New(false) + if d.Language == nil { + panic("language must be set") + } + + ctx := New(langs.GetLocation(d.Language), false) ns := &internal.TemplateFuncsNamespace{ Name: name,