ref: fe6676c775b8d917a661238f24fd4a9088f25d50
parent: 7a97d3e6bca1e30826e1662b5f299b66aa8ab385
author: Bjørn Erik Pedersen <[email protected]>
date: Sun Sep 9 06:15:11 EDT 2018
tpl/collections: Improve type handling in collections.Slice Fixes #5188
--- a/common/collections/collections.go
+++ b/common/collections/collections.go
@@ -19,3 +19,10 @@
type Grouper interface {
Group(key interface{}, items interface{}) (interface{}, error)
}
+
+// Slicer definse a very generic way to create a typed slice. This is used
+// in collections.Slice template func to get types such as Pages, PageGroups etc.
+// instead of the less useful []interface{}.
+type Slicer interface {
+ Slice(items []interface{}) (interface{}, error)
+}
--- /dev/null
+++ b/hugolib/collections.go
@@ -1,0 +1,78 @@
+// Copyright 2018 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 hugolib
+
+import (
+ "fmt"
+
+ "github.com/gohugoio/hugo/common/collections"
+)
+
+var (
+ _ collections.Grouper = (*Page)(nil)
+ _ collections.Slicer = (*Page)(nil)
+ _ collections.Slicer = PageGroup{}
+ _ collections.Slicer = WeightedPage{}
+)
+
+// collections.Slicer implementations below. We keep these bridge implementations
+// here as it makes it easier to get an idea of "type coverage". These
+// implementations have no value on their own.
+
+// Slice is not meant to be used externally. It's a bridge function
+// for the template functions. See collections.Slice.
+func (p *Page) Slice(items []interface{}) (interface{}, error) {
+ return toPages(items)
+}
+
+// Slice is not meant to be used externally. It's a bridge function
+// for the template functions. See collections.Slice.
+func (p PageGroup) Slice(items []interface{}) (interface{}, error) {
+ groups := make(PagesGroup, len(items))
+ for i, v := range items {
+ g, ok := v.(PageGroup)
+ if !ok {
+ return nil, fmt.Errorf("type %T is not a PageGroup", v)
+ }
+ groups[i] = g
+ }
+ return groups, nil
+}
+
+// Slice is not meant to be used externally. It's a bridge function
+// for the template functions. See collections.Slice.
+func (p WeightedPage) Slice(items []interface{}) (interface{}, error) {
+ weighted := make(WeightedPages, len(items))
+ for i, v := range items {
+ g, ok := v.(WeightedPage)
+ if !ok {
+ return nil, fmt.Errorf("type %T is not a WeightedPage", v)
+ }
+ weighted[i] = g
+ }
+ return weighted, nil
+}
+
+// collections.Grouper implementations below
+
+// Group creates a PageGroup from a key and a Pages object
+// This method is not meant for external use. It got its non-typed arguments to satisfy
+// a very generic interface in the tpl package.
+func (p *Page) Group(key interface{}, in interface{}) (interface{}, error) {
+ pages, err := toPages(in)
+ if err != nil {
+ return nil, err
+ }
+ return PageGroup{Key: key, Pages: pages}, nil
+}
--- /dev/null
+++ b/hugolib/collections_test.go
@@ -1,0 +1,88 @@
+// Copyright 2018 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 hugolib
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestGroupFunc(t *testing.T) {
+ assert := require.New(t)
+
+ pageContent := `
+---
+title: "Page"
+---
+
+`
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().
+ WithContent("page1.md", pageContent, "page2.md", pageContent).
+ WithTemplatesAdded("index.html", `
+{{ $cool := .Site.RegularPages | group "cool" }}
+{{ $cool.Key }}: {{ len $cool.Pages }}
+
+`)
+ b.CreateSites().Build(BuildCfg{})
+
+ assert.Equal(1, len(b.H.Sites))
+ require.Len(t, b.H.Sites[0].RegularPages, 2)
+
+ b.AssertFileContent("public/index.html", "cool: 2")
+}
+
+func TestSliceFunc(t *testing.T) {
+ assert := require.New(t)
+
+ pageContent := `
+---
+title: "Page"
+tags: ["blue", "green"]
+tags_weight: %d
+---
+
+`
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().
+ WithContent("page1.md", fmt.Sprintf(pageContent, 10), "page2.md", fmt.Sprintf(pageContent, 20)).
+ WithTemplatesAdded("index.html", `
+{{ $cool := first 1 .Site.RegularPages | group "cool" }}
+{{ $blue := after 1 .Site.RegularPages | group "blue" }}
+{{ $weightedPages := index (index .Site.Taxonomies "tags") "blue" }}
+
+{{ $p1 := index .Site.RegularPages 0 }}{{ $p2 := index .Site.RegularPages 1 }}
+{{ $wp1 := index $weightedPages 0 }}{{ $wp2 := index $weightedPages 1 }}
+
+{{ $pages := slice $p1 $p2 }}
+{{ $pageGroups := slice $cool $blue }}
+{{ $weighted := slice $wp1 $wp2 }}
+
+{{ printf "pages:%d:%T:%v/%v" (len $pages) $pages (index $pages 0) (index $pages 1) }}
+{{ printf "pageGroups:%d:%T:%v/%v" (len $pageGroups) $pageGroups (index (index $pageGroups 0).Pages 0) (index (index $pageGroups 1).Pages 0)}}
+{{ printf "weightedPages:%d::%T:%v" (len $weighted) $weighted $weighted | safeHTML }}
+
+`)
+ b.CreateSites().Build(BuildCfg{})
+
+ assert.Equal(1, len(b.H.Sites))
+ require.Len(t, b.H.Sites[0].RegularPages, 2)
+
+ b.AssertFileContent("public/index.html",
+ "pages:2:hugolib.Pages:Page(/page1.md)/Page(/page2.md)",
+ "pageGroups:2:hugolib.PagesGroup:Page(/page1.md)/Page(/page2.md)",
+ `weightedPages:2::hugolib.WeightedPages:[WeightedPage(10,"Page") WeightedPage(20,"Page")]`)
+}
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -23,7 +23,6 @@
"github.com/gohugoio/hugo/media"
- "github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/langs"
@@ -71,8 +70,6 @@
// Assert that it implements the interface needed for related searches.
_ related.Document = (*Page)(nil)
-
- _ collections.Grouper = Page{}
)
const (
--- a/hugolib/pageGroup.go
+++ b/hugolib/pageGroup.go
@@ -296,14 +296,3 @@
}
return p.groupByDateField(sorter, formatter, order...)
}
-
-// Group creates a PageGroup from a key and a Pages object
-// This method is not meant for external use. It got its non-typed arguments to satisfy
-// a very generic interface in the tpl package.
-func (p Page) Group(key interface{}, in interface{}) (interface{}, error) {
- pages, err := toPages(in)
- if err != nil {
- return nil, err
- }
- return PageGroup{Key: key, Pages: pages}, nil
-}
--- a/hugolib/pageGroup_test.go
+++ b/hugolib/pageGroup_test.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The Hugo Authors. All rights reserved.
+// Copyright 2018 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.
@@ -20,7 +20,6 @@
"testing"
"github.com/spf13/cast"
- "github.com/stretchr/testify/require"
)
type pageGroupTestObject struct {
@@ -455,29 +454,4 @@
if groups != nil {
t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
}
-}
-
-func TestGroupFunc(t *testing.T) {
- assert := require.New(t)
-
- pageContent := `
----
-title: "Page"
----
-
-`
- b := newTestSitesBuilder(t)
- b.WithSimpleConfigFile().
- WithContent("page1.md", pageContent, "page2.md", pageContent).
- WithTemplatesAdded("index.html", `
-{{ $cool := .Site.RegularPages | group "cool" }}
-{{ $cool.Key }}: {{ len $cool.Pages }}
-
-`)
- b.CreateSites().Build(BuildCfg{})
-
- assert.Equal(1, len(b.H.Sites))
- require.Len(t, b.H.Sites[0].RegularPages, 2)
-
- b.AssertFileContent("public/index.html", "cool: 2")
}
--- a/hugolib/pagination.go
+++ b/hugolib/pagination.go
@@ -453,20 +453,34 @@
return Pages{}, nil
}
- switch seq.(type) {
+ switch v := seq.(type) {
case Pages:
- return seq.(Pages), nil
+ return v, nil
case *Pages:
- return *(seq.(*Pages)), nil
+ return *(v), nil
case []*Page:
- return Pages(seq.([]*Page)), nil
+ return Pages(v), nil
case WeightedPages:
- return (seq.(WeightedPages)).Pages(), nil
+ return v.Pages(), nil
case PageGroup:
- return (seq.(PageGroup)).Pages, nil
- default:
- return nil, fmt.Errorf("unsupported type in paginate, got %T", seq)
+ return v.Pages, nil
+ case []interface{}:
+ pages := make(Pages, len(v))
+ success := true
+ for i, vv := range v {
+ p, ok := vv.(*Page)
+ if !ok {
+ success = false
+ break
+ }
+ pages[i] = p
+ }
+ if success {
+ return pages, nil
+ }
}
+
+ return nil, fmt.Errorf("cannot convert type %T to Pages", seq)
}
// probablyEqual checks page lists for probable equality.
--- a/tpl/collections/collections.go
+++ b/tpl/collections/collections.go
@@ -319,18 +319,10 @@
return nil, errors.New("nil is not a valid key to group by")
}
- tp := reflect.TypeOf(items)
- switch tp.Kind() {
- case reflect.Array, reflect.Slice:
- tp = tp.Elem()
- if tp.Kind() == reflect.Ptr {
- tp = tp.Elem()
- }
- in := reflect.New(tp).Interface()
- switch vv := in.(type) {
- case collections.Grouper:
- return vv.Group(key, items)
- }
+ in := newSliceElement(items)
+
+ if g, ok := in.(collections.Grouper); ok {
+ return g.Group(key, items)
}
return nil, fmt.Errorf("grouping not supported for type %T", items)
@@ -514,7 +506,33 @@
}
// Slice returns a slice of all passed arguments.
-func (ns *Namespace) Slice(args ...interface{}) []interface{} {
+func (ns *Namespace) Slice(args ...interface{}) interface{} {
+ if len(args) == 0 {
+ return args
+ }
+
+ first := args[0]
+ allTheSame := true
+ if len(args) > 1 {
+ // This can be a mix of types.
+ firstType := reflect.TypeOf(first)
+ for i := 1; i < len(args); i++ {
+ if firstType != reflect.TypeOf(args[i]) {
+ allTheSame = false
+ break
+ }
+ }
+ }
+
+ if allTheSame {
+ if g, ok := first.(collections.Slicer); ok {
+ v, err := g.Slice(args)
+ if err == nil {
+ return v
+ }
+ }
+ }
+
return args
}
--- a/tpl/collections/collections_test.go
+++ b/tpl/collections/collections_test.go
@@ -25,6 +25,7 @@
"testing"
"time"
+ "github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers"
@@ -110,6 +111,8 @@
{"b", []tstGrouper2{tstGrouper2{}, tstGrouper2{}}, "b(2)"},
{"a", []*tstGrouper{}, "a(0)"},
{"a", []string{"a", "b"}, false},
+ {"a", "asdf", false},
+ {"a", nil, false},
{nil, []*tstGrouper{&tstGrouper{}, &tstGrouper{}}, false},
} {
errMsg := fmt.Sprintf("[%d] %v", i, test)
@@ -633,6 +636,22 @@
}
}
+var _ collections.Slicer = (*tstSlicer)(nil)
+
+type tstSlicer struct {
+ name string
+}
+
+func (p *tstSlicer) Slice(items []interface{}) (interface{}, error) {
+ result := make(tstSlicers, len(items))
+ for i, v := range items {
+ result[i] = v.(*tstSlicer)
+ }
+ return result, nil
+}
+
+type tstSlicers []*tstSlicer
+
func TestSlice(t *testing.T) {
t.Parallel()
@@ -639,19 +658,25 @@
ns := New(&deps.Deps{})
for i, test := range []struct {
- args []interface{}
+ args []interface{}
+ expected interface{}
}{
- {[]interface{}{"a", "b"}},
- // errors
- {[]interface{}{5, "b"}},
- {[]interface{}{tstNoStringer{}}},
+ {[]interface{}{"a", "b"}, []interface{}{"a", "b"}},
+ {[]interface{}{&tstSlicer{"a"}, &tstSlicer{"b"}}, tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
+ {[]interface{}{&tstSlicer{"a"}, "b"}, []interface{}{&tstSlicer{"a"}, "b"}},
+ {[]interface{}{}, []interface{}{}},
+ {[]interface{}{nil}, []interface{}{nil}},
+ {[]interface{}{5, "b"}, []interface{}{5, "b"}},
+ {[]interface{}{tstNoStringer{}}, []interface{}{tstNoStringer{}}},
} {
errMsg := fmt.Sprintf("[%d] %v", i, test.args)
result := ns.Slice(test.args...)
- assert.Equal(t, test.args, result, errMsg)
+ assert.Equal(t, test.expected, result, errMsg)
}
+
+ assert.Len(t, ns.Slice(), 0)
}
func TestUnion(t *testing.T) {
--- a/tpl/collections/reflect_helpers.go
+++ b/tpl/collections/reflect_helpers.go
@@ -102,6 +102,23 @@
}
+func newSliceElement(items interface{}) interface{} {
+ tp := reflect.TypeOf(items)
+ if tp == nil {
+ return nil
+ }
+ switch tp.Kind() {
+ case reflect.Array, reflect.Slice:
+ tp = tp.Elem()
+ if tp.Kind() == reflect.Ptr {
+ tp = tp.Elem()
+ }
+
+ return reflect.New(tp).Interface()
+ }
+ return nil
+}
+
func isNumber(kind reflect.Kind) bool {
return isInt(kind) || isUint(kind) || isFloat(kind)
}