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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

pageable.gno

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