Skip to content

Commit

Permalink
Merge branch 'master' into feat/md-front-matter-meta
Browse files Browse the repository at this point in the history
  • Loading branch information
alexiscolin authored Feb 25, 2025
2 parents 135c016 + 6bf3889 commit 8a1d9f4
Show file tree
Hide file tree
Showing 4 changed files with 442 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/gno.land/p/jeronimoalbi/pager/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/jeronimoalbi/pager
204 changes: 204 additions & 0 deletions examples/gno.land/p/jeronimoalbi/pager/pager.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Package pager provides pagination functionality through a generic pager implementation.
//
// Example usage:
//
// import (
// "strconv"
// "strings"
//
// "gno.land/p/jeronimoalbi/pager"
// )
//
// func Render(path string) string {
// // Define the items to paginate
// items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
//
// // Create a pager that paginates 4 items at a time
// p, err := pager.New(path, len(items), pager.WithPageSize(4))
// if err != nil {
// panic(err)
// }
//
// // Render items for the current page
// var output strings.Builder
// p.Iterate(func(i int) bool {
// output.WriteString("- " + strconv.Itoa(items[i]) + "\n")
// return false
// })
//
// // Render page picker
// if p.HasPages() {
// output.WriteString("\n" + pager.Picker(p))
// }
//
// return output.String()
// }
package pager

import (
"errors"
"math"
"net/url"
"strconv"
"strings"
)

var ErrInvalidPageNumber = errors.New("invalid page number")

// PagerIterFn defines a callback to iterate page items.
type PagerIterFn func(index int) (stop bool)

// New creates a new pager.
func New(rawURL string, totalItems int, options ...PagerOption) (Pager, error) {
u, err := url.Parse(rawURL)
if err != nil {
return Pager{}, err
}

p := Pager{
query: u.RawQuery,
pageQueryParam: DefaultPageQueryParam,
pageSize: DefaultPageSize,
page: 1,
totalItems: totalItems,
}
for _, apply := range options {
apply(&p)
}

p.pageCount = int(math.Ceil(float64(p.totalItems) / float64(p.pageSize)))

rawPage := u.Query().Get(p.pageQueryParam)
if rawPage != "" {
p.page, _ = strconv.Atoi(rawPage)
if p.page == 0 || p.page > p.pageCount {
return Pager{}, ErrInvalidPageNumber
}
}

return p, nil
}

// MustNew creates a new pager or panics if there is an error.
func MustNew(rawURL string, totalItems int, options ...PagerOption) Pager {
p, err := New(rawURL, totalItems, options...)
if err != nil {
panic(err)
}
return p
}

// Pager allows paging items.
type Pager struct {
query, pageQueryParam string
pageSize, page, pageCount, totalItems int
}

// TotalItems returns the total number of items to paginate.
func (p Pager) TotalItems() int {
return p.totalItems
}

// PageSize returns the size of each page.
func (p Pager) PageSize() int {
return p.pageSize
}

// Page returns the current page number.
func (p Pager) Page() int {
return p.page
}

// PageCount returns the number pages.
func (p Pager) PageCount() int {
return p.pageCount
}

// Offset returns the index of the first page item.
func (p Pager) Offset() int {
return (p.page - 1) * p.pageSize
}

// HasPages checks if pager has more than one page.
func (p Pager) HasPages() bool {
return p.pageCount > 1
}

// GetPageURI returns the URI for a page.
// An empty string is returned when page doesn't exist.
func (p Pager) GetPageURI(page int) string {
if page < 1 || page > p.PageCount() {
return ""
}

values, _ := url.ParseQuery(p.query)
values.Set(p.pageQueryParam, strconv.Itoa(page))
return "?" + values.Encode()
}

// PrevPageURI returns the URI path to the previous page.
// An empty string is returned when current page is the first page.
func (p Pager) PrevPageURI() string {
if p.page == 1 || !p.HasPages() {
return ""
}
return p.GetPageURI(p.page - 1)
}

// NextPageURI returns the URI path to the next page.
// An empty string is returned when current page is the last page.
func (p Pager) NextPageURI() string {
if p.page == p.pageCount {
// Current page is the last page
return ""
}
return p.GetPageURI(p.page + 1)
}

// Iterate allows iterating page items.
func (p Pager) Iterate(fn PagerIterFn) bool {
if p.totalItems == 0 {
return true
}

start := p.Offset()
end := start + p.PageSize()
if end > p.totalItems {
end = p.totalItems
}

for i := start; i < end; i++ {
if fn(i) {
return true
}
}
return false
}

// TODO: Support different types of pickers (ex. with clickable page numbers)

// Picker returns a string with the pager as Markdown.
// An empty string is returned when the pager has no pages.
func Picker(p Pager) string {
if !p.HasPages() {
return ""
}

var out strings.Builder

if s := p.PrevPageURI(); s != "" {
out.WriteString("[«](" + s + ") | ")
} else {
out.WriteString("\\- | ")
}

out.WriteString("page " + strconv.Itoa(p.Page()) + " of " + strconv.Itoa(p.PageCount()))

if s := p.NextPageURI(); s != "" {
out.WriteString(" | [»](" + s + ")")
} else {
out.WriteString(" | \\-")
}

return out.String()
}
33 changes: 33 additions & 0 deletions examples/gno.land/p/jeronimoalbi/pager/pager_options.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package pager

import "strings"

const (
DefaultPageSize = 50
DefaultPageQueryParam = "page"
)

// PagerOption configures the pager.
type PagerOption func(*Pager)

// WithPageSize assigns a page size to a pager.
func WithPageSize(size int) PagerOption {
return func(p *Pager) {
if size < 1 {
p.pageSize = DefaultPageSize
} else {
p.pageSize = size
}
}
}

// WithPageQueryParam assigns the name of the URL query param for the page value.
func WithPageQueryParam(name string) PagerOption {
return func(p *Pager) {
name = strings.TrimSpace(name)
if name == "" {
name = DefaultPageQueryParam
}
p.pageQueryParam = name
}
}
Loading

0 comments on commit 8a1d9f4

Please sign in to comment.