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 #9979
  • Loading branch information
bep committed Jun 7, 2022
1 parent 534e715 commit 0566bbf
Show file tree
Hide file tree
Showing 18 changed files with 216 additions and 87 deletions.
40 changes: 40 additions & 0 deletions common/hreflect/helpers.go
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,44 @@ 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
}

// AsTime returns v as a time.Time if possible.
// The given location is only used if the value implements AsTimeProvider (e.g. go-toml local).
// A zero Time and false is returned if this isn't possible.
// Note that this function does not accept string dates.
func AsTime(v reflect.Value, loc *time.Location) (time.Time, bool) {
if v.Kind() == reflect.Interface {
return AsTime(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
12 changes: 7 additions & 5 deletions common/htime/time.go
Expand Up @@ -20,8 +20,6 @@ import (
"github.com/bep/clock"
"github.com/spf13/cast"

toml "github.com/pelletier/go-toml/v2"

"github.com/gohugoio/locales"
)

Expand Down Expand Up @@ -139,13 +137,12 @@ func (f TimeFormatter) Format(t time.Time, layout string) string {

func ToTimeInDefaultLocationE(i any, location *time.Location) (tim time.Time, err error) {
switch vv := i.(type) {
case toml.LocalDate:
return vv.AsTime(location), nil
case toml.LocalDateTime:
case AsTimeProvider:
return vv.AsTime(location), nil
// issue #8895
// datetimes parsed by `go-toml` have empty zone name
// convert back them into string and use `cast`
// TODO(bep) add tests, make sure we really need this.
case time.Time:
i = vv.Format(time.RFC3339)
}
Expand All @@ -161,3 +158,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
}
59 changes: 59 additions & 0 deletions hugolib/dates_test.go
Expand Up @@ -214,3 +214,62 @@ func TestTimeOnError(t *testing.T) {
b.Assert(b.BuildE(BuildCfg{}), qt.Not(qt.IsNil))

}

func TestTOMLDates(t *testing.T) {
t.Parallel()

files := `
-- config.toml --
timeZone = "America/Los_Angeles"
-- content/_index.md --
---
date: "2020-10-20"
---
-- content/p1.md --
+++
title = "TOML Date with UTC offset"
date = 2021-08-16T06:00:00+00:00
+++
## 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 }}
{{ $p1Date := (site.GetPage "p1").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 }}
Full time: {{ $p1Date | time.Format ":time_full" }}
`

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
Full time: 6:00:00 am UTC
`)
}
4 changes: 3 additions & 1 deletion tpl/collections/append_test.go
Expand Up @@ -18,15 +18,17 @@ import (
"testing"

qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/langs"
)

// Also see tests in common/collection.
func TestAppend(t *testing.T) {
t.Parallel()
c := qt.New(t)

ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
start any
Expand Down
4 changes: 3 additions & 1 deletion tpl/collections/apply_test.go
Expand Up @@ -21,7 +21,9 @@ import (
"testing"

qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/tpl"
)
Expand Down Expand Up @@ -67,7 +69,7 @@ func (templateFinder) GetFunc(name string) (reflect.Value, bool) {
func TestApply(t *testing.T) {
t.Parallel()
c := qt.New(t)
d := &deps.Deps{}
d := &deps.Deps{Language: langs.NewDefaultLanguage(config.New())}
d.SetTmpl(new(templateFinder))
ns := New(d)

Expand Down
16 changes: 14 additions & 2 deletions tpl/collections/collections.go
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
37 changes: 19 additions & 18 deletions tpl/collections/collections_test.go
Expand Up @@ -40,7 +40,7 @@ func TestAfter(t *testing.T) {
t.Parallel()
c := qt.New(t)

ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
index any
Expand Down Expand Up @@ -97,7 +97,7 @@ func (g *tstGrouper2) Group(key any, items any) (any, error) {
func TestGroup(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
key any
Expand Down Expand Up @@ -187,7 +187,7 @@ func TestDelimit(t *testing.T) {
func TestDictionary(t *testing.T) {
c := qt.New(t)

ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
values []any
Expand Down Expand Up @@ -226,7 +226,7 @@ func TestDictionary(t *testing.T) {
func TestReverse(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

s := []string{"a", "b", "c"}
reversed, err := ns.Reverse(s)
Expand All @@ -245,7 +245,7 @@ func TestEchoParam(t *testing.T) {
t.Parallel()
c := qt.New(t)

ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
a any
Expand Down Expand Up @@ -277,7 +277,7 @@ func TestFirst(t *testing.T) {
t.Parallel()
c := qt.New(t)

ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
limit any
Expand Down Expand Up @@ -315,7 +315,7 @@ func TestIn(t *testing.T) {
t.Parallel()
c := qt.New(t)

ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
l1 any
Expand Down Expand Up @@ -391,7 +391,7 @@ func TestIntersect(t *testing.T) {
t.Parallel()
c := qt.New(t)

ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
l1, l2 any
Expand Down Expand Up @@ -518,7 +518,7 @@ func TestLast(t *testing.T) {
t.Parallel()
c := qt.New(t)

ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
limit any
Expand Down Expand Up @@ -557,7 +557,7 @@ func TestLast(t *testing.T) {
func TestQuerify(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
params []any
Expand Down Expand Up @@ -591,7 +591,7 @@ func TestQuerify(t *testing.T) {
}

func BenchmarkQuerify(b *testing.B) {
ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
params := []any{"a", "b", "c", "d", "f", " &"}

b.ResetTimer()
Expand All @@ -604,7 +604,7 @@ func BenchmarkQuerify(b *testing.B) {
}

func BenchmarkQuerifySlice(b *testing.B) {
ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
params := []string{"a", "b", "c", "d", "f", " &"}

b.ResetTimer()
Expand All @@ -619,7 +619,7 @@ func BenchmarkQuerifySlice(b *testing.B) {
func TestSeq(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
args []any
Expand Down Expand Up @@ -663,7 +663,7 @@ func TestSeq(t *testing.T) {
func TestShuffle(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
seq any
Expand Down Expand Up @@ -703,7 +703,7 @@ func TestShuffle(t *testing.T) {
func TestShuffleRandomising(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

// Note that this test can fail with false negative result if the shuffle
// of the sequence happens to be the same as the original sequence. However
Expand Down Expand Up @@ -734,7 +734,7 @@ func TestShuffleRandomising(t *testing.T) {
func TestSlice(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
args []any
Expand All @@ -758,7 +758,7 @@ func TestUnion(t *testing.T) {
t.Parallel()
c := qt.New(t)

ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})

for i, test := range []struct {
l1 any
Expand Down Expand Up @@ -847,7 +847,7 @@ func TestUnion(t *testing.T) {
func TestUniq(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := New(&deps.Deps{})
ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
for i, test := range []struct {
l any
expect any
Expand Down Expand Up @@ -979,6 +979,7 @@ func newDeps(cfg config.Provider) *deps.Deps {
panic(err)
}
return &deps.Deps{
Language: l,
Cfg: cfg,
Fs: hugofs.NewMem(l),
ContentSpec: cs,
Expand Down

0 comments on commit 0566bbf

Please sign in to comment.