Skip to content

Commit

Permalink
Fix raw TOML dates in where/eq
Browse files Browse the repository at this point in the history
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 gohugoio#9979
  • Loading branch information
bep committed Jun 6, 2022
1 parent 534e715 commit 4d7c62a
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 41 deletions.
39 changes: 39 additions & 0 deletions common/hreflect/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import (
"context"
"reflect"
"sync"
"time"

"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/types"
)

Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions common/htime/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
50 changes: 50 additions & 0 deletions hugolib/dates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
`)
}
16 changes: 14 additions & 2 deletions tpl/collections/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions tpl/collections/reflect_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package collections
import (
"fmt"
"reflect"
"time"

"errors"

Expand All @@ -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) {
Expand Down
11 changes: 5 additions & 6 deletions tpl/collections/sort.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
24 changes: 10 additions & 14 deletions tpl/collections/where.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
}
34 changes: 18 additions & 16 deletions tpl/compare/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
}
7 changes: 6 additions & 1 deletion tpl/compare/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ package compare

import (
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/tpl/internal"
)

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,
Expand Down

0 comments on commit 4d7c62a

Please sign in to comment.