hugo/hugolib/pagination.go
Vincent Danjean cfda13b363 hugolib: Allow creating page groups from any page collection
This also adjusts the pagination logic to allow for these new collections.

Note that we will follow up with a template function named `group` that will be the end user API. The `.Group` method on `Page` should be considered as internal.

Updates #4865
2018-09-08 11:14:09 +02:00

581 lines
13 KiB
Go

// Copyright 2015 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 (
"errors"
"fmt"
"html/template"
"math"
"reflect"
"strings"
"github.com/gohugoio/hugo/config"
"github.com/spf13/cast"
)
// Pager represents one of the elements in a paginator.
// The number, starting on 1, represents its place.
type Pager struct {
number int
*paginator
}
func (p Pager) String() string {
return fmt.Sprintf("Pager %d", p.number)
}
type paginatedElement interface {
Len() int
}
// Len returns the number of pages in the list.
func (p Pages) Len() int {
return len(p)
}
// 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
}
type pagers []*Pager
var (
paginatorEmptyPages Pages
paginatorEmptyPageGroups PagesGroup
)
type paginator struct {
paginatedElements []paginatedElement
pagers
paginationURLFactory
total int
size int
source interface{}
options []interface{}
}
type paginationURLFactory func(int) string
// PageNumber returns the current page's number in the pager sequence.
func (p *Pager) PageNumber() int {
return p.number
}
// URL returns the URL to the current page.
func (p *Pager) URL() template.HTML {
return template.HTML(p.paginationURLFactory(p.PageNumber()))
}
// Pages returns the Pages on this page.
// Note: If this return a non-empty result, then PageGroups() will return empty.
func (p *Pager) Pages() Pages {
if len(p.paginatedElements) == 0 {
return paginatorEmptyPages
}
if pages, ok := p.element().(Pages); ok {
return pages
}
return paginatorEmptyPages
}
// PageGroups return Page groups for this page.
// Note: If this return non-empty result, then Pages() will return empty.
func (p *Pager) PageGroups() PagesGroup {
if len(p.paginatedElements) == 0 {
return paginatorEmptyPageGroups
}
if groups, ok := p.element().(PagesGroup); ok {
return groups
}
return paginatorEmptyPageGroups
}
func (p *Pager) element() paginatedElement {
if len(p.paginatedElements) == 0 {
return paginatorEmptyPages
}
return p.paginatedElements[p.PageNumber()-1]
}
// page returns the Page with the given index
func (p *Pager) page(index int) (*Page, error) {
if pages, ok := p.element().(Pages); ok {
if pages != nil && len(pages) > index {
return pages[index], nil
}
return nil, nil
}
// must be PagesGroup
// this construction looks clumsy, but ...
// ... it is the difference between 99.5% and 100% test coverage :-)
groups := p.element().(PagesGroup)
i := 0
for _, v := range groups {
for _, page := range v.Pages {
if i == index {
return page, nil
}
i++
}
}
return nil, nil
}
// NumberOfElements gets the number of elements on this page.
func (p *Pager) NumberOfElements() int {
return p.element().Len()
}
// HasPrev tests whether there are page(s) before the current.
func (p *Pager) HasPrev() bool {
return p.PageNumber() > 1
}
// Prev returns the pager for the previous page.
func (p *Pager) Prev() *Pager {
if !p.HasPrev() {
return nil
}
return p.pagers[p.PageNumber()-2]
}
// HasNext tests whether there are page(s) after the current.
func (p *Pager) HasNext() bool {
return p.PageNumber() < len(p.paginatedElements)
}
// Next returns the pager for the next page.
func (p *Pager) Next() *Pager {
if !p.HasNext() {
return nil
}
return p.pagers[p.PageNumber()]
}
// First returns the pager for the first page.
func (p *Pager) First() *Pager {
return p.pagers[0]
}
// Last returns the pager for the last page.
func (p *Pager) Last() *Pager {
return p.pagers[len(p.pagers)-1]
}
// Pagers returns a list of pagers that can be used to build a pagination menu.
func (p *paginator) Pagers() pagers {
return p.pagers
}
// PageSize returns the size of each paginator page.
func (p *paginator) PageSize() int {
return p.size
}
// TotalPages returns the number of pages in the paginator.
func (p *paginator) TotalPages() int {
return len(p.paginatedElements)
}
// TotalNumberOfElements returns the number of elements on all pages in this paginator.
func (p *paginator) TotalNumberOfElements() int {
return p.total
}
func splitPages(pages Pages, size int) []paginatedElement {
var split []paginatedElement
for low, j := 0, len(pages); low < j; low += size {
high := int(math.Min(float64(low+size), float64(len(pages))))
split = append(split, pages[low:high])
}
return split
}
func splitPageGroups(pageGroups PagesGroup, size int) []paginatedElement {
type keyPage struct {
key interface{}
page *Page
}
var (
split []paginatedElement
flattened []keyPage
)
for _, g := range pageGroups {
for _, p := range g.Pages {
flattened = append(flattened, keyPage{g.Key, p})
}
}
numPages := len(flattened)
for low, j := 0, numPages; low < j; low += size {
high := int(math.Min(float64(low+size), float64(numPages)))
var (
pg PagesGroup
key interface{}
groupIndex = -1
)
for k := low; k < high; k++ {
kp := flattened[k]
if key == nil || key != kp.key {
key = kp.key
pg = append(pg, PageGroup{Key: key})
groupIndex++
}
pg[groupIndex].Pages = append(pg[groupIndex].Pages, kp.page)
}
split = append(split, pg)
}
return split
}
// Paginator get this Page's main output's paginator.
func (p *Page) Paginator(options ...interface{}) (*Pager, error) {
return p.mainPageOutput.Paginator(options...)
}
// Paginator gets this PageOutput's paginator if it's already created.
// If it's not, one will be created with all pages in Data["Pages"].
func (p *PageOutput) Paginator(options ...interface{}) (*Pager, error) {
if !p.IsNode() {
return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.title)
}
pagerSize, err := resolvePagerSize(p.s.Cfg, options...)
if err != nil {
return nil, err
}
var initError error
p.paginatorInit.Do(func() {
if p.paginator != nil {
return
}
pathDescriptor := p.targetPathDescriptor
if p.s.owner.IsMultihost() {
pathDescriptor.LangPrefix = ""
}
pagers, err := paginatePages(pathDescriptor, p.data["Pages"], pagerSize)
if err != nil {
initError = err
}
if len(pagers) > 0 {
// the rest of the nodes will be created later
p.paginator = pagers[0]
p.paginator.source = "paginator"
p.paginator.options = options
}
})
if initError != nil {
return nil, initError
}
return p.paginator, nil
}
// Paginate invokes this Page's main output's Paginate method.
func (p *Page) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
return p.mainPageOutput.Paginate(seq, options...)
}
// Paginate gets this PageOutput's paginator if it's already created.
// If it's not, one will be created with the qiven sequence.
// Note that repeated calls will return the same result, even if the sequence is different.
func (p *PageOutput) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
if !p.IsNode() {
return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.title)
}
pagerSize, err := resolvePagerSize(p.s.Cfg, options...)
if err != nil {
return nil, err
}
var initError error
p.paginatorInit.Do(func() {
if p.paginator != nil {
return
}
pathDescriptor := p.targetPathDescriptor
if p.s.owner.IsMultihost() {
pathDescriptor.LangPrefix = ""
}
pagers, err := paginatePages(pathDescriptor, seq, pagerSize)
if err != nil {
initError = err
}
if len(pagers) > 0 {
// the rest of the nodes will be created later
p.paginator = pagers[0]
p.paginator.source = seq
p.paginator.options = options
}
})
if initError != nil {
return nil, initError
}
if p.paginator.source == "paginator" {
return nil, errors.New("a Paginator was previously built for this Node without filters; look for earlier .Paginator usage")
}
if !reflect.DeepEqual(options, p.paginator.options) || !probablyEqualPageLists(p.paginator.source, seq) {
return nil, errors.New("invoked multiple times with different arguments")
}
return p.paginator, nil
}
func resolvePagerSize(cfg config.Provider, options ...interface{}) (int, error) {
if len(options) == 0 {
return cfg.GetInt("paginate"), nil
}
if len(options) > 1 {
return -1, errors.New("too many arguments, 'pager size' is currently the only option")
}
pas, err := cast.ToIntE(options[0])
if err != nil || pas <= 0 {
return -1, errors.New(("'pager size' must be a positive integer"))
}
return pas, nil
}
func paginatePages(td targetPathDescriptor, seq interface{}, pagerSize int) (pagers, error) {
if pagerSize <= 0 {
return nil, errors.New("'paginate' configuration setting must be positive to paginate")
}
urlFactory := newPaginationURLFactory(td)
var paginator *paginator
groups, err := toPagesGroup(seq)
if err != nil {
return nil, err
}
if groups != nil {
paginator, _ = newPaginatorFromPageGroups(groups, pagerSize, urlFactory)
} else {
pages, err := toPages(seq)
if err != nil {
return nil, err
}
paginator, _ = newPaginatorFromPages(pages, pagerSize, urlFactory)
}
pagers := paginator.Pagers()
return pagers, nil
}
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(pagesGroup), nil
}
}
return nil, nil
}
func toPages(seq interface{}) (Pages, error) {
if seq == nil {
return Pages{}, nil
}
switch seq.(type) {
case Pages:
return seq.(Pages), nil
case *Pages:
return *(seq.(*Pages)), nil
case []*Page:
return Pages(seq.([]*Page)), nil
case WeightedPages:
return (seq.(WeightedPages)).Pages(), nil
case PageGroup:
return (seq.(PageGroup)).Pages, nil
default:
return nil, fmt.Errorf("unsupported type in paginate, got %T", seq)
}
}
// probablyEqual checks page lists for probable equality.
// It may return false positives.
// The motivation behind this is to avoid potential costly reflect.DeepEqual
// when "probably" is good enough.
func probablyEqualPageLists(a1 interface{}, a2 interface{}) bool {
if a1 == nil || a2 == nil {
return a1 == a2
}
t1 := reflect.TypeOf(a1)
t2 := reflect.TypeOf(a2)
if t1 != t2 {
return false
}
if g1, ok := a1.(PagesGroup); ok {
g2 := a2.(PagesGroup)
if len(g1) != len(g2) {
return false
}
if len(g1) == 0 {
return true
}
if g1.Len() != g2.Len() {
return false
}
return g1[0].Pages[0] == g2[0].Pages[0]
}
p1, err1 := toPages(a1)
p2, err2 := toPages(a2)
// probably the same wrong type
if err1 != nil && err2 != nil {
return true
}
if len(p1) != len(p2) {
return false
}
if len(p1) == 0 {
return true
}
return p1[0] == p2[0]
}
func newPaginatorFromPages(pages Pages, size int, urlFactory paginationURLFactory) (*paginator, error) {
if size <= 0 {
return nil, errors.New("Paginator size must be positive")
}
split := splitPages(pages, size)
return newPaginator(split, len(pages), size, urlFactory)
}
func newPaginatorFromPageGroups(pageGroups PagesGroup, size int, urlFactory paginationURLFactory) (*paginator, error) {
if size <= 0 {
return nil, errors.New("Paginator size must be positive")
}
split := splitPageGroups(pageGroups, size)
return newPaginator(split, pageGroups.Len(), size, urlFactory)
}
func newPaginator(elements []paginatedElement, total, size int, urlFactory paginationURLFactory) (*paginator, error) {
p := &paginator{total: total, paginatedElements: elements, size: size, paginationURLFactory: urlFactory}
var ps pagers
if len(elements) > 0 {
ps = make(pagers, len(elements))
for i := range p.paginatedElements {
ps[i] = &Pager{number: (i + 1), paginator: p}
}
} else {
ps = make(pagers, 1)
ps[0] = &Pager{number: 1, paginator: p}
}
p.pagers = ps
return p, nil
}
func newPaginationURLFactory(d targetPathDescriptor) paginationURLFactory {
return func(page int) string {
pathDescriptor := d
var rel string
if page > 1 {
rel = fmt.Sprintf("/%s/%d/", d.PathSpec.PaginatePath, page)
pathDescriptor.Addends = rel
}
targetPath := createTargetPath(pathDescriptor)
targetPath = strings.TrimSuffix(targetPath, d.Type.BaseFilename())
link := d.PathSpec.PrependBasePath(targetPath)
// Note: The targetPath is massaged with MakePathSanitized
return d.PathSpec.URLizeFilename(link)
}
}