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 }