hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

pagination.go (9840B)

    1 // Copyright 2019 The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 package page
   15 
   16 import (
   17 	"errors"
   18 	"fmt"
   19 	"html/template"
   20 	"math"
   21 	"reflect"
   22 
   23 	"github.com/gohugoio/hugo/config"
   24 
   25 	"github.com/spf13/cast"
   26 )
   27 
   28 // PaginatorProvider provides two ways to create a page paginator.
   29 type PaginatorProvider interface {
   30 	Paginator(options ...any) (*Pager, error)
   31 	Paginate(seq any, options ...any) (*Pager, error)
   32 }
   33 
   34 // Pager represents one of the elements in a paginator.
   35 // The number, starting on 1, represents its place.
   36 type Pager struct {
   37 	number int
   38 	*Paginator
   39 }
   40 
   41 func (p Pager) String() string {
   42 	return fmt.Sprintf("Pager %d", p.number)
   43 }
   44 
   45 type paginatedElement interface {
   46 	Len() int
   47 }
   48 
   49 type pagers []*Pager
   50 
   51 var (
   52 	paginatorEmptyPages      Pages
   53 	paginatorEmptyPageGroups PagesGroup
   54 )
   55 
   56 type Paginator struct {
   57 	paginatedElements []paginatedElement
   58 	pagers
   59 	paginationURLFactory
   60 	total int
   61 	size  int
   62 	rev   bool
   63 }
   64 
   65 type paginationURLFactory func(int, int) string
   66 
   67 // PageNumber returns the current page's number in the pager sequence.
   68 func (p *Pager) PageNumber() int {
   69 	return p.number
   70 }
   71 
   72 // URL returns the URL to the current page.
   73 func (p *Pager) URL() template.HTML {
   74 	return template.HTML(p.paginationURLFactory(p.PageNumber(), p.TotalPages()))
   75 }
   76 
   77 // Pages returns the Pages on this page.
   78 // Note: If this return a non-empty result, then PageGroups() will return empty.
   79 func (p *Pager) Pages() Pages {
   80 	if len(p.paginatedElements) == 0 {
   81 		return paginatorEmptyPages
   82 	}
   83 
   84 	if pages, ok := p.element().(Pages); ok {
   85 		return pages
   86 	}
   87 
   88 	return paginatorEmptyPages
   89 }
   90 
   91 // PageGroups return Page groups for this page.
   92 // Note: If this return non-empty result, then Pages() will return empty.
   93 func (p *Pager) PageGroups() PagesGroup {
   94 	if len(p.paginatedElements) == 0 {
   95 		return paginatorEmptyPageGroups
   96 	}
   97 
   98 	if groups, ok := p.element().(PagesGroup); ok {
   99 		return groups
  100 	}
  101 
  102 	return paginatorEmptyPageGroups
  103 }
  104 
  105 func (p *Pager) element() paginatedElement {
  106 	if len(p.paginatedElements) == 0 {
  107 		return paginatorEmptyPages
  108 	}
  109 	return p.paginatedElements[p.PageNumber()-1]
  110 }
  111 
  112 // page returns the Page with the given index
  113 func (p *Pager) page(index int) (Page, error) {
  114 	if pages, ok := p.element().(Pages); ok {
  115 		if pages != nil && len(pages) > index {
  116 			return pages[index], nil
  117 		}
  118 		return nil, nil
  119 	}
  120 
  121 	// must be PagesGroup
  122 	// this construction looks clumsy, but ...
  123 	// ... it is the difference between 99.5% and 100% test coverage :-)
  124 	groups := p.element().(PagesGroup)
  125 
  126 	i := 0
  127 	for _, v := range groups {
  128 		for _, page := range v.Pages {
  129 			if i == index {
  130 				return page, nil
  131 			}
  132 			i++
  133 		}
  134 	}
  135 	return nil, nil
  136 }
  137 
  138 // NumberOfElements gets the number of elements on this page.
  139 func (p *Pager) NumberOfElements() int {
  140 	return p.element().Len()
  141 }
  142 
  143 // HasPrev tests whether there are page(s) before the current.
  144 func (p *Pager) HasPrev() bool {
  145 	return (!p.rev && p.PageNumber() > 1) || (p.rev && p.PageNumber() < p.TotalPages())
  146 }
  147 
  148 // Prev returns the pager for the previous page.
  149 func (p *Pager) Prev() *Pager {
  150 	if !p.HasPrev() {
  151 		return nil
  152 	}
  153 	if !p.rev {
  154 		return p.pagers[p.PageNumber()-2]
  155 	} else {
  156 		return p.pagers[p.TotalPages()-1-p.PageNumber()]
  157 	}
  158 }
  159 
  160 // HasNext tests whether there are page(s) after the current.
  161 func (p *Pager) HasNext() bool {
  162 	return (!p.rev && p.PageNumber() < len(p.paginatedElements)) || (p.rev && p.PageNumber() > 1)
  163 }
  164 
  165 // Next returns the pager for the next page.
  166 func (p *Pager) Next() *Pager {
  167 	if !p.HasNext() {
  168 		return nil
  169 	}
  170 	if !p.rev {
  171 		return p.pagers[p.PageNumber()]
  172 	} else {
  173 		return p.pagers[p.TotalPages()+1-p.PageNumber()]
  174 	}
  175 }
  176 
  177 // First returns the pager for the first page.
  178 func (p *Pager) First() *Pager {
  179 	return p.pagers[0]
  180 }
  181 
  182 // Last returns the pager for the last page.
  183 func (p *Pager) Last() *Pager {
  184 	return p.pagers[len(p.pagers)-1]
  185 }
  186 
  187 // Pagers returns a list of pagers that can be used to build a pagination menu.
  188 func (p *Paginator) Pagers() pagers {
  189 	return p.pagers
  190 }
  191 
  192 // PageSize returns the size of each paginator page.
  193 func (p *Paginator) PageSize() int {
  194 	return p.size
  195 }
  196 
  197 // TotalPages returns the number of pages in the paginator.
  198 func (p *Paginator) TotalPages() int {
  199 	return len(p.paginatedElements)
  200 }
  201 
  202 // TotalNumberOfElements returns the number of elements on all pages in this paginator.
  203 func (p *Paginator) TotalNumberOfElements() int {
  204 	return p.total
  205 }
  206 
  207 func splitPages(pages Pages, size int) []paginatedElement {
  208 	var split []paginatedElement
  209 	for low, j := 0, len(pages); low < j; low += size {
  210 		high := int(math.Min(float64(low+size), float64(len(pages))))
  211 		split = append(split, pages[low:high])
  212 	}
  213 
  214 	return split
  215 }
  216 
  217 func splitPageGroups(pageGroups PagesGroup, size int) []paginatedElement {
  218 	type keyPage struct {
  219 		key  any
  220 		page Page
  221 	}
  222 
  223 	var (
  224 		split     []paginatedElement
  225 		flattened []keyPage
  226 	)
  227 
  228 	for _, g := range pageGroups {
  229 		for _, p := range g.Pages {
  230 			flattened = append(flattened, keyPage{g.Key, p})
  231 		}
  232 	}
  233 
  234 	numPages := len(flattened)
  235 
  236 	for low, j := 0, numPages; low < j; low += size {
  237 		high := int(math.Min(float64(low+size), float64(numPages)))
  238 
  239 		var (
  240 			pg         PagesGroup
  241 			key        any
  242 			groupIndex = -1
  243 		)
  244 
  245 		for k := low; k < high; k++ {
  246 			kp := flattened[k]
  247 			if key == nil || key != kp.key {
  248 				key = kp.key
  249 				pg = append(pg, PageGroup{Key: key})
  250 				groupIndex++
  251 			}
  252 			pg[groupIndex].Pages = append(pg[groupIndex].Pages, kp.page)
  253 		}
  254 		split = append(split, pg)
  255 	}
  256 
  257 	return split
  258 }
  259 
  260 func ResolvePagerSize(cfg config.Provider, options ...any) (int, bool, error) {
  261 	if len(options) == 0 {
  262 		return cfg.GetInt("paginate"), cfg.GetBool("paginateRev"), nil
  263 	}
  264 
  265 	if len(options) > 2 {
  266 		return -1, false, errors.New("too many arguments, 'pager size' and 'rev' are currently the only options")
  267 	}
  268 
  269 	pas, err := cast.ToIntE(options[0])
  270 
  271 	if err != nil || pas <= 0 {
  272 		return -1, false, errors.New(("'pager size' must be a positive integer"))
  273 	}
  274 
  275 	if len(options) == 1 {
  276 		return pas, cfg.GetBool("paginateRev"), nil
  277 	}
  278 
  279 	rev, err := cast.ToBoolE(options[1])
  280 
  281 	if err != nil {
  282 		return -1, false, errors.New(("'rev' must be a bool"))
  283 	}
  284 
  285 	return pas, rev, nil
  286 }
  287 
  288 func Paginate(td TargetPathDescriptor, seq any, pagerSize int, rev bool) (*Paginator, error) {
  289 	if pagerSize <= 0 {
  290 		return nil, errors.New("'paginate' configuration setting must be positive to paginate")
  291 	}
  292 
  293 	urlFactory := newPaginationURLFactory(td, rev)
  294 
  295 	var paginator *Paginator
  296 
  297 	groups, err := ToPagesGroup(seq)
  298 	if err != nil {
  299 		return nil, err
  300 	}
  301 	if groups != nil {
  302 		paginator, _ = newPaginatorFromPageGroups(groups, pagerSize, rev, urlFactory)
  303 	} else {
  304 		pages, err := ToPages(seq)
  305 		if err != nil {
  306 			return nil, err
  307 		}
  308 		paginator, _ = newPaginatorFromPages(pages, pagerSize, rev, urlFactory)
  309 	}
  310 
  311 	return paginator, nil
  312 }
  313 
  314 // probablyEqual checks page lists for probable equality.
  315 // It may return false positives.
  316 // The motivation behind this is to avoid potential costly reflect.DeepEqual
  317 // when "probably" is good enough.
  318 func probablyEqualPageLists(a1 any, a2 any) bool {
  319 	if a1 == nil || a2 == nil {
  320 		return a1 == a2
  321 	}
  322 
  323 	t1 := reflect.TypeOf(a1)
  324 	t2 := reflect.TypeOf(a2)
  325 
  326 	if t1 != t2 {
  327 		return false
  328 	}
  329 
  330 	if g1, ok := a1.(PagesGroup); ok {
  331 		g2 := a2.(PagesGroup)
  332 		if len(g1) != len(g2) {
  333 			return false
  334 		}
  335 		if len(g1) == 0 {
  336 			return true
  337 		}
  338 		if g1.Len() != g2.Len() {
  339 			return false
  340 		}
  341 
  342 		return g1[0].Pages[0] == g2[0].Pages[0]
  343 	}
  344 
  345 	p1, err1 := ToPages(a1)
  346 	p2, err2 := ToPages(a2)
  347 
  348 	// probably the same wrong type
  349 	if err1 != nil && err2 != nil {
  350 		return true
  351 	}
  352 
  353 	if len(p1) != len(p2) {
  354 		return false
  355 	}
  356 
  357 	if len(p1) == 0 {
  358 		return true
  359 	}
  360 
  361 	return p1[0] == p2[0]
  362 }
  363 
  364 func newPaginatorFromPages(pages Pages, size int, rev bool, urlFactory paginationURLFactory) (*Paginator, error) {
  365 	if size <= 0 {
  366 		return nil, errors.New("Paginator size must be positive")
  367 	}
  368 
  369 	split := splitPages(pages, size)
  370 
  371 	return newPaginator(split, len(pages), size, rev, urlFactory)
  372 }
  373 
  374 func newPaginatorFromPageGroups(pageGroups PagesGroup, size int, rev bool, urlFactory paginationURLFactory) (*Paginator, error) {
  375 	if size <= 0 {
  376 		return nil, errors.New("Paginator size must be positive")
  377 	}
  378 
  379 	split := splitPageGroups(pageGroups, size)
  380 
  381 	return newPaginator(split, pageGroups.Len(), size, rev, urlFactory)
  382 }
  383 
  384 func newPaginator(elements []paginatedElement, total, size int, rev bool, urlFactory paginationURLFactory) (*Paginator, error) {
  385 	p := &Paginator{total: total, paginatedElements: elements, size: size, paginationURLFactory: urlFactory, rev: rev}
  386 
  387 	var ps pagers
  388 
  389 	if len(elements) > 0 {
  390 		ps = make(pagers, len(elements))
  391 		for i := range p.paginatedElements {
  392 			if !rev {
  393 				ps[i] = &Pager{number: (i + 1), Paginator: p}
  394 			} else {
  395 				ps[len(elements)-1-i] = &Pager{number: (i + 1), Paginator: p}
  396 			}
  397 		}
  398 	} else {
  399 		ps = make(pagers, 1)
  400 		ps[0] = &Pager{number: 1, Paginator: p}
  401 	}
  402 
  403 	p.pagers = ps
  404 
  405 	return p, nil
  406 }
  407 
  408 func newPaginationURLFactory(d TargetPathDescriptor, rev bool) paginationURLFactory {
  409 	return func(pageNumber int, totalPages int) string {
  410 		pathDescriptor := d
  411 		var rel string
  412 		if (!rev && pageNumber > 1) || (rev && pageNumber < totalPages) {
  413 			rel = fmt.Sprintf("/%s/%d/", d.PathSpec.PaginatePath, pageNumber)
  414 			pathDescriptor.Addends = rel
  415 		}
  416 
  417 		return CreateTargetPaths(pathDescriptor).RelPermalink(d.PathSpec)
  418 	}
  419 }