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
"context"
"reflect"
"sync"
+ "time"
+ "github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/types"
)
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 {
"github.com/bep/clock"
"github.com/spf13/cast"
- toml "github.com/pelletier/go-toml/v2"
-
"github.com/gohugoio/locales"
)
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)
}
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
+}
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
+`)
+}
"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.
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
"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"
)
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)
"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"
)
// 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.
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
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
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
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)
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
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
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
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
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
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
}
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()
}
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()
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
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
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
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
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
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
panic(err)
}
return &deps.Deps{
+ Language: l,
Cfg: cfg,
Fs: hugofs.NewMem(l),
ContentSpec: cs,
"reflect"
"testing"
+ "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/langs"
qt "github.com/frankban/quicktest"
)
c := qt.New(t)
- ns := New(&deps.Deps{})
+ ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
s1 := []TstX{{A: "a"}, {A: "b"}, {A: "d"}, {A: "e"}}
s2 := []TstX{{A: "b"}, {A: "e"}}
"testing"
"github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/langs"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/deps"
func TestIndex(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 {
item any
"testing"
"github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
)
func TestMerge(t *testing.T) {
- ns := New(&deps.Deps{})
+ ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
simpleMap := map[string]any{"a": 1, "b": 2}
func TestMergeDataFormats(t *testing.T) {
c := qt.New(t)
- ns := New(&deps.Deps{})
+ ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
toml1 := `
V1 = "v1_1"
import (
"fmt"
"reflect"
- "time"
"errors"
var (
zero reflect.Value
errorType = reflect.TypeOf((*error)(nil)).Elem()
- timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
)
func numberToFloat(v reflect.Value) (float64, error) {
"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 {
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
// 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
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
"reflect"
"testing"
+ "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/langs"
qt "github.com/frankban/quicktest"
)
c := qt.New(t)
- ns := New(&deps.Deps{})
+ ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
s1 := []TstX{{A: "a"}, {A: "b"}}
s2 := []TstX{{A: "a"}, {A: "e"}}
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:
}
}
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:
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.AsTime(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()
}
"time"
"github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/deps"
)
func TestWhere(t *testing.T) {
t.Parallel()
- ns := New(&deps.Deps{})
+ ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
type Mid struct {
Tst TstX
func TestCheckCondition(t *testing.T) {
t.Parallel()
- ns := New(&deps.Deps{})
+ ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
type expect struct {
result bool
"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
}
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:
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
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
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.AsTime(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()
}
then := time.Now()
now := time.Now()
- ns := New(false)
+ ns := New(time.UTC, false)
for i, test := range []struct {
dflt any
func TestCompare(t *testing.T) {
t.Parallel()
- n := New(false)
+ n := New(time.UTC, false)
twoEq := func(a, b any) bool {
return n.Eq(a, b)
t.Parallel()
c := qt.New(t)
- ns := New(false)
+ ns := New(time.UTC, false)
for _, test := range []struct {
first any
t.Parallel()
c := qt.New(t)
- ns := New(false)
+ ns := New(time.UTC, false)
for _, test := range []struct {
first any
t.Parallel()
c := qt.New(t)
- ns := New(false)
+ ns := New(time.UTC, false)
for _, test := range []struct {
first any
t.Parallel()
c := qt.New(t)
- ns := New(false)
+ ns := New(time.UTC, false)
for _, test := range []struct {
first any
t.Parallel()
c := qt.New(t)
- ns := New(false)
+ ns := New(time.UTC, false)
for _, test := range []struct {
first any
t.Parallel()
c := qt.New(t)
- ns := New(false)
+ ns := New(time.UTC, false)
for _, test := range []struct {
first any
func TestCase(t *testing.T) {
c := qt.New(t)
- n := New(false)
+ n := New(time.UTC, false)
c.Assert(n.Eq("az", "az"), qt.Equals, true)
c.Assert(n.Eq("az", stringType("az")), qt.Equals, true)
func TestStringType(t *testing.T) {
c := qt.New(t)
- n := New(true)
+ n := New(time.UTC, true)
c.Assert(n.Lt("az", "Za"), qt.Equals, true)
c.Assert(n.Gt("ab", "Ab"), qt.Equals, true)
func TestTimeUnix(t *testing.T) {
t.Parallel()
+ n := New(time.UTC, false)
var sec int64 = 1234567890
tv := reflect.ValueOf(time.Unix(sec, 0))
i := 1
- res := toTimeUnix(tv)
+ res := n.toTimeUnix(tv)
if sec != res {
t.Errorf("[%d] timeUnix got %v but expected %v", i, res, sec)
}
}
}()
iv := reflect.ValueOf(sec)
- toTimeUnix(iv)
+ n.toTimeUnix(iv)
}(t)
}
func TestConditional(t *testing.T) {
c := qt.New(t)
- n := New(false)
+ n := New(time.UTC, false)
a, b := "a", "b"
c.Assert(n.Conditional(true, a, b), qt.Equals, a)
t.Parallel()
c := qt.New(t)
- ns := New(false)
+ ns := New(time.UTC, false)
panicMsg := "missing arguments for comparison"
import (
"github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/tpl/internal"
)
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,