From 6667c6d7430acc16b3683fbbacd263f1d00c8672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sat, 8 Sep 2018 13:00:36 +0200 Subject: [PATCH] tpl/collections: Add group template func This extends the page grouping in Hugo with a template function that allows for ad-hoc grouping. A made-up example: ``` {{ $cool := where .Site.RegularPages "Params.cool" true | group "cool" }} {{ $blue := where .Site.RegularPages "Params.blue" true | group "blue" }} {{ $paginator := .Paginate (slice $cool $blue) }} ``` Closes #4865 --- common/collections/collections.go | 21 +++++++++++++++ common/types/types.go | 2 +- hugolib/page.go | 3 +++ hugolib/pageGroup.go | 12 ++++++--- hugolib/pageGroup_test.go | 26 ++++++++++++++++++ tpl/collections/collections.go | 25 ++++++++++++++++++ tpl/collections/collections_test.go | 41 +++++++++++++++++++++++++++++ tpl/collections/init.go | 5 ++++ 8 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 common/collections/collections.go diff --git a/common/collections/collections.go b/common/collections/collections.go new file mode 100644 index 000000000..bb47c8acc --- /dev/null +++ b/common/collections/collections.go @@ -0,0 +1,21 @@ +// 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 collections contains common Hugo functionality related to collection +// handling. +package collections + +// Grouper defines a very generic way to group items by a given key. +type Grouper interface { + Group(key interface{}, items interface{}) (interface{}, error) +} diff --git a/common/types/types.go b/common/types/types.go index fca58edcb..ca74391f8 100644 --- a/common/types/types.go +++ b/common/types/types.go @@ -1,4 +1,4 @@ -// Copyright 2017-present 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. diff --git a/hugolib/page.go b/hugolib/page.go index bb6dab8e0..e03cebdd7 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -23,6 +23,7 @@ import ( "github.com/gohugoio/hugo/media" + "github.com/gohugoio/hugo/common/collections" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/langs" @@ -70,6 +71,8 @@ var ( // Assert that it implements the interface needed for related searches. _ related.Document = (*Page)(nil) + + _ collections.Grouper = Page{} ) const ( diff --git a/hugolib/pageGroup.go b/hugolib/pageGroup.go index f12eff253..24d007c25 100644 --- a/hugolib/pageGroup.go +++ b/hugolib/pageGroup.go @@ -298,8 +298,12 @@ func (p Pages) GroupByParamDate(key string, format string, order ...string) (Pag } // Group creates a PageGroup from a key and a Pages object -func (p *Page) Group(key interface{}, pages Pages) (PageGroup, error) { - pageGroup := PageGroup{Key: key, Pages: pages} - - return pageGroup, nil +// 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 } diff --git a/hugolib/pageGroup_test.go b/hugolib/pageGroup_test.go index d17e09f8b..832d6a2dd 100644 --- a/hugolib/pageGroup_test.go +++ b/hugolib/pageGroup_test.go @@ -20,6 +20,7 @@ import ( "testing" "github.com/spf13/cast" + "github.com/stretchr/testify/require" ) type pageGroupTestObject struct { @@ -455,3 +456,28 @@ func TestGroupByParamDateWithEmptyPages(t *testing.T) { 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") +} diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go index 51bc5c796..2ccee43d4 100644 --- a/tpl/collections/collections.go +++ b/tpl/collections/collections.go @@ -23,6 +23,7 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/common/collections" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/deps" @@ -311,6 +312,30 @@ func (ns *Namespace) Intersect(l1, l2 interface{}) (interface{}, error) { } } +// Group groups a set of elements by the given key. +// This is currently only supported for Pages. +func (ns *Namespace) Group(key interface{}, items interface{}) (interface{}, error) { + if key == nil { + 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.Zero(tp).Interface() + switch vv := in.(type) { + case collections.Grouper: + return vv.Group(key, items) + } + } + + return nil, fmt.Errorf("grouping not supported for type %T", items) +} + // IsSet returns whether a given array, channel, slice, or map has a key // defined. func (ns *Namespace) IsSet(a interface{}, key interface{}) (bool, error) { diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go index c3e88f36f..cbcf819c7 100644 --- a/tpl/collections/collections_test.go +++ b/tpl/collections/collections_test.go @@ -75,6 +75,47 @@ func TestAfter(t *testing.T) { } } +type tstGrouper struct { +} + +type tstGroupers []*tstGrouper + +func (g tstGrouper) Group(key interface{}, items interface{}) (interface{}, error) { + ilen := reflect.ValueOf(items).Len() + return fmt.Sprintf("%v(%d)", key, ilen), nil +} + +func TestGroup(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + key interface{} + items interface{} + expect interface{} + }{ + {"a", []*tstGrouper{&tstGrouper{}, &tstGrouper{}}, "a(2)"}, + {"b", tstGroupers{&tstGrouper{}, &tstGrouper{}}, "b(2)"}, + {"a", []tstGrouper{tstGrouper{}, tstGrouper{}}, "a(2)"}, + {"a", []*tstGrouper{}, "a(0)"}, + {"a", []string{"a", "b"}, false}, + {nil, []*tstGrouper{&tstGrouper{}, &tstGrouper{}}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Group(test.key, test.items) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + require.Equal(t, test.expect, result, errMsg) + } +} + func TestDelimit(t *testing.T) { t.Parallel() diff --git a/tpl/collections/init.go b/tpl/collections/init.go index b986b3b42..ad4f6f207 100644 --- a/tpl/collections/init.go +++ b/tpl/collections/init.go @@ -138,6 +138,11 @@ func init() { [][2]string{}, ) + ns.AddMethodMapping(ctx.Group, + []string{"group"}, + [][2]string{}, + ) + ns.AddMethodMapping(ctx.Seq, []string{"seq"}, [][2]string{