Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}