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 }