pager.gno
4.52 Kb · 204 lines
1// Package pager provides pagination functionality through a generic pager implementation.
2//
3// Example usage:
4//
5// import (
6// "strconv"
7// "strings"
8//
9// "gno.land/p/jeronimoalbi/pager"
10// )
11//
12// func Render(path string) string {
13// // Define the items to paginate
14// items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
15//
16// // Create a pager that paginates 4 items at a time
17// p, err := pager.New(path, len(items), pager.WithPageSize(4))
18// if err != nil {
19// panic(err)
20// }
21//
22// // Render items for the current page
23// var output strings.Builder
24// p.Iterate(func(i int) bool {
25// output.WriteString("- " + strconv.Itoa(items[i]) + "\n")
26// return false
27// })
28//
29// // Render page picker
30// if p.HasPages() {
31// output.WriteString("\n" + pager.Picker(p))
32// }
33//
34// return output.String()
35// }
36package pager
37
38import (
39 "errors"
40 "math"
41 "net/url"
42 "strconv"
43 "strings"
44)
45
46var ErrInvalidPageNumber = errors.New("invalid page number")
47
48// PagerIterFn defines a callback to iterate page items.
49type PagerIterFn func(index int) (stop bool)
50
51// New creates a new pager.
52func New(rawURL string, totalItems int, options ...PagerOption) (Pager, error) {
53 u, err := url.Parse(rawURL)
54 if err != nil {
55 return Pager{}, err
56 }
57
58 p := Pager{
59 query: u.RawQuery,
60 pageQueryParam: DefaultPageQueryParam,
61 pageSize: DefaultPageSize,
62 page: 1,
63 totalItems: totalItems,
64 }
65 for _, apply := range options {
66 apply(&p)
67 }
68
69 p.pageCount = int(math.Ceil(float64(p.totalItems) / float64(p.pageSize)))
70
71 rawPage := u.Query().Get(p.pageQueryParam)
72 if rawPage != "" {
73 p.page, _ = strconv.Atoi(rawPage)
74 if p.page == 0 || p.page > p.pageCount {
75 return Pager{}, ErrInvalidPageNumber
76 }
77 }
78
79 return p, nil
80}
81
82// MustNew creates a new pager or panics if there is an error.
83func MustNew(rawURL string, totalItems int, options ...PagerOption) Pager {
84 p, err := New(rawURL, totalItems, options...)
85 if err != nil {
86 panic(err)
87 }
88 return p
89}
90
91// Pager allows paging items.
92type Pager struct {
93 query, pageQueryParam string
94 pageSize, page, pageCount, totalItems int
95}
96
97// TotalItems returns the total number of items to paginate.
98func (p Pager) TotalItems() int {
99 return p.totalItems
100}
101
102// PageSize returns the size of each page.
103func (p Pager) PageSize() int {
104 return p.pageSize
105}
106
107// Page returns the current page number.
108func (p Pager) Page() int {
109 return p.page
110}
111
112// PageCount returns the number pages.
113func (p Pager) PageCount() int {
114 return p.pageCount
115}
116
117// Offset returns the index of the first page item.
118func (p Pager) Offset() int {
119 return (p.page - 1) * p.pageSize
120}
121
122// HasPages checks if pager has more than one page.
123func (p Pager) HasPages() bool {
124 return p.pageCount > 1
125}
126
127// GetPageURI returns the URI for a page.
128// An empty string is returned when page doesn't exist.
129func (p Pager) GetPageURI(page int) string {
130 if page < 1 || page > p.PageCount() {
131 return ""
132 }
133
134 values, _ := url.ParseQuery(p.query)
135 values.Set(p.pageQueryParam, strconv.Itoa(page))
136 return "?" + values.Encode()
137}
138
139// PrevPageURI returns the URI path to the previous page.
140// An empty string is returned when current page is the first page.
141func (p Pager) PrevPageURI() string {
142 if p.page == 1 || !p.HasPages() {
143 return ""
144 }
145 return p.GetPageURI(p.page - 1)
146}
147
148// NextPageURI returns the URI path to the next page.
149// An empty string is returned when current page is the last page.
150func (p Pager) NextPageURI() string {
151 if p.page == p.pageCount {
152 // Current page is the last page
153 return ""
154 }
155 return p.GetPageURI(p.page + 1)
156}
157
158// Iterate allows iterating page items.
159func (p Pager) Iterate(fn PagerIterFn) bool {
160 if p.totalItems == 0 {
161 return true
162 }
163
164 start := p.Offset()
165 end := start + p.PageSize()
166 if end > p.totalItems {
167 end = p.totalItems
168 }
169
170 for i := start; i < end; i++ {
171 if fn(i) {
172 return true
173 }
174 }
175 return false
176}
177
178// TODO: Support different types of pickers (ex. with clickable page numbers)
179
180// Picker returns a string with the pager as Markdown.
181// An empty string is returned when the pager has no pages.
182func Picker(p Pager) string {
183 if !p.HasPages() {
184 return ""
185 }
186
187 var out strings.Builder
188
189 if s := p.PrevPageURI(); s != "" {
190 out.WriteString("[«](" + s + ") | ")
191 } else {
192 out.WriteString("\\- | ")
193 }
194
195 out.WriteString("page " + strconv.Itoa(p.Page()) + " of " + strconv.Itoa(p.PageCount()))
196
197 if s := p.NextPageURI(); s != "" {
198 out.WriteString(" | [»](" + s + ")")
199 } else {
200 out.WriteString(" | \\-")
201 }
202
203 return out.String()
204}