diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-01-02 12:33:26 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-03-23 18:51:22 +0100 |
commit | 597e418cb02883418f2cebb41400e8e61413f651 (patch) | |
tree | 177ad9c540b2583b6dab138c9f0490d28989c7f7 /resources/page/pagegroup.go | |
parent | 44f5c1c14cb1f42cc5f01739c289e9cfc83602af (diff) |
Make Page an interface
The main motivation of this commit is to add a `page.Page` interface to replace the very file-oriented `hugolib.Page` struct.
This is all a preparation step for issue #5074, "pages from other data sources".
But this also fixes a set of annoying limitations, especially related to custom output formats, and shortcodes.
Most notable changes:
* The inner content of shortcodes using the `{{%` as the outer-most delimiter will now be sent to the content renderer, e.g. Blackfriday.
This means that any markdown will partake in the global ToC and footnote context etc.
* The Custom Output formats are now "fully virtualized". This removes many of the current limitations.
* The taxonomy list type now has a reference to the `Page` object.
This improves the taxonomy template `.Title` situation and make common template constructs much simpler.
See #5074
Fixes #5763
Fixes #5758
Fixes #5090
Fixes #5204
Fixes #4695
Fixes #5607
Fixes #5707
Fixes #5719
Fixes #3113
Fixes #5706
Fixes #5767
Fixes #5723
Fixes #5769
Fixes #5770
Fixes #5771
Fixes #5759
Fixes #5776
Fixes #5777
Fixes #5778
Diffstat (limited to 'resources/page/pagegroup.go')
-rw-r--r-- | resources/page/pagegroup.go | 369 |
1 files changed, 369 insertions, 0 deletions
diff --git a/resources/page/pagegroup.go b/resources/page/pagegroup.go new file mode 100644 index 000000000..46d9bd174 --- /dev/null +++ b/resources/page/pagegroup.go @@ -0,0 +1,369 @@ +// Copyright 2019 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 page + +import ( + "errors" + "fmt" + "reflect" + "sort" + "strings" + "time" + + "github.com/gohugoio/hugo/common/collections" + + "github.com/gohugoio/hugo/resources/resource" +) + +var ( + _ collections.Slicer = PageGroup{} +) + +// PageGroup represents a group of pages, grouped by the key. +// The key is typically a year or similar. +type PageGroup struct { + Key interface{} + Pages +} + +type mapKeyValues []reflect.Value + +func (v mapKeyValues) Len() int { return len(v) } +func (v mapKeyValues) Swap(i, j int) { v[i], v[j] = v[j], v[i] } + +type mapKeyByInt struct{ mapKeyValues } + +func (s mapKeyByInt) Less(i, j int) bool { return s.mapKeyValues[i].Int() < s.mapKeyValues[j].Int() } + +type mapKeyByStr struct{ mapKeyValues } + +func (s mapKeyByStr) Less(i, j int) bool { + return s.mapKeyValues[i].String() < s.mapKeyValues[j].String() +} + +func sortKeys(v []reflect.Value, order string) []reflect.Value { + if len(v) <= 1 { + return v + } + + switch v[0].Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if order == "desc" { + sort.Sort(sort.Reverse(mapKeyByInt{v})) + } else { + sort.Sort(mapKeyByInt{v}) + } + case reflect.String: + if order == "desc" { + sort.Sort(sort.Reverse(mapKeyByStr{v})) + } else { + sort.Sort(mapKeyByStr{v}) + } + } + return v +} + +// PagesGroup represents a list of page groups. +// This is what you get when doing page grouping in the templates. +type PagesGroup []PageGroup + +// Reverse reverses the order of this list of page groups. +func (p PagesGroup) Reverse() PagesGroup { + for i, j := 0, len(p)-1; i < j; i, j = i+1, j-1 { + p[i], p[j] = p[j], p[i] + } + + return p +} + +var ( + errorType = reflect.TypeOf((*error)(nil)).Elem() + pagePtrType = reflect.TypeOf((*Page)(nil)).Elem() + pagesType = reflect.TypeOf(Pages{}) +) + +// GroupBy groups by the value in the given field or method name and with the given order. +// Valid values for order is asc, desc, rev and reverse. +func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) { + if len(p) < 1 { + return nil, nil + } + + direction := "asc" + + if len(order) > 0 && (strings.ToLower(order[0]) == "desc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse") { + direction = "desc" + } + + var ft interface{} + m, ok := pagePtrType.MethodByName(key) + if ok { + if m.Type.NumOut() == 0 || m.Type.NumOut() > 2 { + return nil, errors.New(key + " is a Page method but you can't use it with GroupBy") + } + if m.Type.NumOut() == 1 && m.Type.Out(0).Implements(errorType) { + return nil, errors.New(key + " is a Page method but you can't use it with GroupBy") + } + if m.Type.NumOut() == 2 && !m.Type.Out(1).Implements(errorType) { + return nil, errors.New(key + " is a Page method but you can't use it with GroupBy") + } + ft = m + } else { + ft, ok = pagePtrType.Elem().FieldByName(key) + if !ok { + return nil, errors.New(key + " is neither a field nor a method of Page") + } + } + + var tmp reflect.Value + switch e := ft.(type) { + case reflect.StructField: + tmp = reflect.MakeMap(reflect.MapOf(e.Type, pagesType)) + case reflect.Method: + tmp = reflect.MakeMap(reflect.MapOf(e.Type.Out(0), pagesType)) + } + + for _, e := range p { + ppv := reflect.ValueOf(e) + var fv reflect.Value + switch ft.(type) { + case reflect.StructField: + fv = ppv.Elem().FieldByName(key) + case reflect.Method: + fv = ppv.MethodByName(key).Call([]reflect.Value{})[0] + } + if !fv.IsValid() { + continue + } + if !tmp.MapIndex(fv).IsValid() { + tmp.SetMapIndex(fv, reflect.MakeSlice(pagesType, 0, 0)) + } + tmp.SetMapIndex(fv, reflect.Append(tmp.MapIndex(fv), ppv)) + } + + sortedKeys := sortKeys(tmp.MapKeys(), direction) + r := make([]PageGroup, len(sortedKeys)) + for i, k := range sortedKeys { + r[i] = PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().(Pages)} + } + + return r, nil +} + +// GroupByParam groups by the given page parameter key's value and with the given order. +// Valid values for order is asc, desc, rev and reverse. +func (p Pages) GroupByParam(key string, order ...string) (PagesGroup, error) { + if len(p) < 1 { + return nil, nil + } + + direction := "asc" + + if len(order) > 0 && (strings.ToLower(order[0]) == "desc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse") { + direction = "desc" + } + + var tmp reflect.Value + var keyt reflect.Type + for _, e := range p { + param := resource.GetParamToLower(e, key) + if param != nil { + if _, ok := param.([]string); !ok { + keyt = reflect.TypeOf(param) + tmp = reflect.MakeMap(reflect.MapOf(keyt, pagesType)) + break + } + } + } + if !tmp.IsValid() { + return nil, errors.New("there is no such a param") + } + + for _, e := range p { + param := resource.GetParam(e, key) + + if param == nil || reflect.TypeOf(param) != keyt { + continue + } + v := reflect.ValueOf(param) + if !tmp.MapIndex(v).IsValid() { + tmp.SetMapIndex(v, reflect.MakeSlice(pagesType, 0, 0)) + } + tmp.SetMapIndex(v, reflect.Append(tmp.MapIndex(v), reflect.ValueOf(e))) + } + + var r []PageGroup + for _, k := range sortKeys(tmp.MapKeys(), direction) { + r = append(r, PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().(Pages)}) + } + + return r, nil +} + +func (p Pages) groupByDateField(sorter func(p Pages) Pages, formatter func(p Page) string, order ...string) (PagesGroup, error) { + if len(p) < 1 { + return nil, nil + } + + sp := sorter(p) + + if !(len(order) > 0 && (strings.ToLower(order[0]) == "asc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse")) { + sp = sp.Reverse() + } + + date := formatter(sp[0].(Page)) + var r []PageGroup + r = append(r, PageGroup{Key: date, Pages: make(Pages, 0)}) + r[0].Pages = append(r[0].Pages, sp[0]) + + i := 0 + for _, e := range sp[1:] { + date = formatter(e.(Page)) + if r[i].Key.(string) != date { + r = append(r, PageGroup{Key: date}) + i++ + } + r[i].Pages = append(r[i].Pages, e) + } + return r, nil +} + +// GroupByDate groups by the given page's Date value in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByDate(format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + return p.ByDate() + } + formatter := func(p Page) string { + return p.Date().Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} + +// GroupByPublishDate groups by the given page's PublishDate value in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByPublishDate(format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + return p.ByPublishDate() + } + formatter := func(p Page) string { + return p.PublishDate().Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} + +// GroupByExpiryDate groups by the given page's ExpireDate value in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByExpiryDate(format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + return p.ByExpiryDate() + } + formatter := func(p Page) string { + return p.ExpiryDate().Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} + +// GroupByParamDate groups by a date set as a param on the page in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByParamDate(key string, format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + var r Pages + for _, e := range p { + param := resource.GetParamToLower(e, key) + if _, ok := param.(time.Time); ok { + r = append(r, e) + } + } + pdate := func(p1, p2 Page) bool { + p1p, p2p := p1.(Page), p2.(Page) + return resource.GetParamToLower(p1p, key).(time.Time).Unix() < resource.GetParamToLower(p2p, key).(time.Time).Unix() + } + pageBy(pdate).Sort(r) + return r + } + formatter := func(p Page) string { + return resource.GetParamToLower(p, key).(time.Time).Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} + +// Slice is not meant to be used externally. It's a bridge function +// for the template functions. See collections.Slice. +func (p PageGroup) Slice(in interface{}) (interface{}, error) { + switch items := in.(type) { + case PageGroup: + return items, nil + case []interface{}: + 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 + default: + return nil, fmt.Errorf("invalid slice type %T", items) + } +} + +// Len returns the number of pages in the page group. +func (psg PagesGroup) Len() int { + l := 0 + for _, pg := range psg { + l += len(pg.Pages) + } + return l +} + +// ToPagesGroup tries to convert seq into a PagesGroup. +func ToPagesGroup(seq interface{}) (PagesGroup, error) { + switch v := seq.(type) { + case nil: + return nil, nil + case PagesGroup: + return v, nil + case []PageGroup: + return PagesGroup(v), nil + case []interface{}: + l := len(v) + if l == 0 { + break + } + switch v[0].(type) { + case PageGroup: + pagesGroup := make(PagesGroup, l) + for i, ipg := range v { + if pg, ok := ipg.(PageGroup); ok { + pagesGroup[i] = pg + } else { + return nil, fmt.Errorf("unsupported type in paginate from slice, got %T instead of PageGroup", ipg) + } + } + return pagesGroup, nil + } + } + + return nil, nil +} |