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}