Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

pageable.gno

7.96 Kb · 301 lines
  1// Package pageable implements a flexible pagination system that can be used with any data structure
  2// that implements the Pageable interface. It provides functionality for:
  3//
  4//   - Paginating through collections of items
  5//   - Configurable page sizes
  6//   - Forward and reverse iteration
  7//   - URL query parameter parsing
  8//   - Markdown-based pagination UI
  9//
 10// This package is currently used by:
 11//   - gno.land/p/nt/avl/pager: implementation for AVL trees.
 12//   - gno.land/p/moul/ulist/pager: implementation for ulist
 13package pageable
 14
 15import (
 16	"math"
 17	"net/url"
 18	"strconv"
 19
 20	"gno.land/p/nt/ufmt/v0"
 21)
 22
 23// Pageable defines the minimal interface required for pagination.
 24// This interface is intentionally lightweight, requiring only two methods.
 25// While some data structures (like AVL trees) might have separate methods for forward
 26// and reverse iteration, this interface consolidates both directions into a single
 27// IterateByOffset method. It's the responsibility of the implementing wrapper to
 28// handle the direction logic internally based on the offset and count parameters.
 29type Pageable interface {
 30	// Size returns the total number of items
 31	Size() int
 32
 33	// IterateByOffset performs iteration starting from offset for count elements.
 34	// The callback receives an index and a value, returns true to stop iteration.
 35	//
 36	// For reverse iteration, the implementation should handle the direction internally
 37	// by adjusting how it interprets the offset and count parameters. For example:
 38	// - In forward mode: offset 0, count 5 would return items [0,1,2,3,4]
 39	// - In reverse mode: offset 0, count -5 would return items [4,3,2,1,0]
 40	//
 41	// This approach allows the interface to remain simple while supporting
 42	// bidirectional iteration through the implementation layer.
 43	IterateByOffset(offset int, count int, cb func(index interface{}, value interface{}) bool) bool
 44}
 45
 46// Pager provides pagination functionality for any Pageable source
 47type Pager struct {
 48	Source          Pageable
 49	PageQueryParam  string
 50	SizeQueryParam  string
 51	DefaultPageSize int
 52	Reversed        bool
 53}
 54
 55// Page represents a single page of results
 56type Page struct {
 57	Items      []Item
 58	PageNumber int
 59	PageSize   int
 60	TotalItems int
 61	TotalPages int
 62	HasPrev    bool
 63	HasNext    bool
 64	Pager      *Pager
 65}
 66
 67// Item represents a generic item in the page
 68type Item struct {
 69	Index interface{}
 70	Value interface{}
 71}
 72
 73// NewPager creates a new Pager with default values
 74func NewPager(source Pageable, defaultPageSize int, reversed bool) *Pager {
 75	return &Pager{
 76		Source:          source,
 77		PageQueryParam:  "page",
 78		SizeQueryParam:  "size",
 79		DefaultPageSize: defaultPageSize,
 80		Reversed:        reversed,
 81	}
 82}
 83
 84// GetPage retrieves a page of results from the AVL tree.
 85func (p *Pager) GetPage(pageNumber int) *Page {
 86	return p.GetPageWithSize(pageNumber, p.DefaultPageSize)
 87}
 88
 89// Helper function to create a basic page
 90func (p *Pager) newBasePage(pageSize int) *Page {
 91	totalItems := p.Source.Size()
 92	totalPages := 0
 93	if pageSize > 0 {
 94		totalPages = int(math.Ceil(float64(totalItems) / float64(pageSize)))
 95	}
 96
 97	return &Page{
 98		TotalItems: totalItems,
 99		TotalPages: totalPages,
100		PageSize:   pageSize,
101		Pager:      p,
102	}
103}
104
105func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page {
106	// Handle invalid page size
107	if pageSize <= 0 {
108		return p.newBasePage(pageSize)
109	}
110
111	page := p.newBasePage(pageSize)
112
113	// For empty source, return empty page with no navigation
114	if page.TotalItems == 0 {
115		page.HasPrev = false
116		page.HasNext = false
117		return page
118	}
119
120	// page number provided is not available
121	if pageNumber < 1 {
122		page.HasNext = page.TotalPages > 0
123		return page
124	}
125
126	// page number provided is outside the range of total pages
127	if pageNumber > page.TotalPages {
128		page.PageNumber = pageNumber
129		page.HasPrev = page.TotalPages > 0
130		return page
131	}
132
133	// Calculate offset and size
134	var offset, iterCount int
135	if p.Reversed {
136		// For reversed order, calculate from the end
137		iterCount = min(pageSize, page.TotalItems-((pageNumber-1)*pageSize))
138		offset = max(0, page.TotalItems-(pageNumber*pageSize))
139	} else {
140		offset = (pageNumber - 1) * pageSize
141		iterCount = pageSize
142	}
143
144	// Collect items for the current page
145	var items []Item
146	if p.Reversed {
147		// For reversed order, use negative count to indicate reverse iteration
148		offset = page.TotalItems - offset - iterCount
149		p.Source.IterateByOffset(offset, -iterCount, func(index interface{}, value interface{}) bool {
150			items = append(items, Item{Index: index, Value: value})
151			return false
152		})
153	} else {
154		p.Source.IterateByOffset(offset, iterCount, func(index interface{}, value interface{}) bool {
155			items = append(items, Item{Index: index, Value: value})
156			return false
157		})
158	}
159
160	page.Items = items
161	page.PageNumber = pageNumber
162	page.HasPrev = pageNumber > 1
163	page.HasNext = pageNumber < page.TotalPages
164	return page
165}
166
167func (p *Pager) MustGetPageByPath(rawURL string) *Page {
168	page, err := p.GetPageByPath(rawURL)
169	if err != nil {
170		panic("invalid path")
171	}
172	return page
173}
174
175// GetPageByPath retrieves a page of results based on the query parameters in the URL path.
176func (p *Pager) GetPageByPath(rawURL string) (*Page, error) {
177	pageNumber, pageSize, err := p.ParseQuery(rawURL)
178	if err != nil {
179		return nil, err
180	}
181	return p.GetPageWithSize(pageNumber, pageSize), nil
182}
183
184// Picker generates the Markdown UI for the page picker using default query string format
185func (p *Page) Picker() string {
186	return p.PickerWithPath("")
187}
188
189// PickerWithPath generates the Markdown UI for the page picker with optional path
190// If path is provided, it preserves existing query parameters (except page)
191func (p *Page) PickerWithPath(path string) string {
192	pageNumber := p.PageNumber
193	pageNumber = max(pageNumber, 1)
194
195	if p.TotalPages <= 1 {
196		return ""
197	}
198
199	// Parse existing query parameters if path is provided
200	baseQuery := ""
201	if path != "" {
202		u, _ := url.Parse(path)
203		if u != nil {
204			query := u.Query()
205			// Remove existing page query parameter
206			query.Del(p.Pager.PageQueryParam)
207			// Encode remaining query parameters
208			baseQuery = query.Encode()
209			if baseQuery != "" {
210				baseQuery = "&" + baseQuery
211			}
212		}
213	}
214
215	md := ""
216
217	if p.HasPrev {
218		// Always show the first page link
219		md += ufmt.Sprintf("[%d](?%s=%d%s) | ", 1, p.Pager.PageQueryParam, 1, baseQuery)
220
221		// Before
222		if p.PageNumber > 4 {
223			md += "… | "
224		}
225
226		if p.PageNumber > 3 {
227			md += ufmt.Sprintf("[%d](?%s=%d%s) | ", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2, baseQuery)
228		}
229
230		if p.PageNumber > 2 {
231			md += ufmt.Sprintf("[%d](?%s=%d%s) | ", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1, baseQuery)
232		}
233	}
234
235	if p.PageNumber > 0 && p.PageNumber <= p.TotalPages {
236		// Current page
237		md += ufmt.Sprintf("**%d**", p.PageNumber)
238	} else {
239		md += ufmt.Sprintf("_%d_", p.PageNumber)
240	}
241
242	if p.HasNext {
243		md += " | "
244
245		if p.PageNumber < p.TotalPages-1 {
246			md += ufmt.Sprintf("[%d](?%s=%d%s) | ", p.PageNumber+1, p.Pager.PageQueryParam, p.PageNumber+1, baseQuery)
247		}
248
249		if p.PageNumber < p.TotalPages-2 {
250			md += ufmt.Sprintf("[%d](?%s=%d%s) | ", p.PageNumber+2, p.Pager.PageQueryParam, p.PageNumber+2, baseQuery)
251		}
252
253		if p.PageNumber < p.TotalPages-3 {
254			md += "… | "
255		}
256
257		// Always show the last page link
258		md += ufmt.Sprintf("[%d](?%s=%d%s)", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages, baseQuery)
259	}
260
261	return md
262}
263
264// Helper to safely parse positive integers with a default value
265func parsePositiveIntOrDefault(str string, defaultValue int) int {
266	if str == "" {
267		return defaultValue
268	}
269	val, err := strconv.Atoi(str)
270	if err != nil || val < 1 {
271		return defaultValue
272	}
273	return val
274}
275
276func (p *Pager) ParseQuery(rawURL string) (int, int, error) {
277	u, err := url.Parse(rawURL)
278	if err != nil {
279		return 1, p.DefaultPageSize, err
280	}
281
282	query := u.Query()
283	pageNumber := parsePositiveIntOrDefault(query.Get(p.PageQueryParam), 1)
284	pageSize := parsePositiveIntOrDefault(query.Get(p.SizeQueryParam), p.DefaultPageSize)
285
286	return pageNumber, pageSize, nil
287}
288
289func max(a, b int) int {
290	if a > b {
291		return a
292	}
293	return b
294}
295
296func min(a, b int) int {
297	if a < b {
298		return a
299	}
300	return b
301}