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}