pager.gno
5.12 Kb · 227 lines
1package pager
2
3import (
4 "math"
5 "net/url"
6 "strconv"
7
8 "gno.land/p/nt/avl/v0/rotree"
9 "gno.land/p/nt/ufmt/v0"
10)
11
12// Pager is a struct that holds the AVL tree and pagination parameters.
13type Pager struct {
14 Tree rotree.IReadOnlyTree
15 PageQueryParam string
16 SizeQueryParam string
17 DefaultPageSize int
18 Reversed bool
19}
20
21// Page represents a single page of results.
22type Page struct {
23 Items []Item
24 PageNumber int
25 PageSize int
26 TotalItems int
27 TotalPages int
28 HasPrev bool
29 HasNext bool
30 Pager *Pager // Reference to the parent Pager
31}
32
33// Item represents a key-value pair in the AVL tree.
34type Item struct {
35 Key string
36 Value any
37}
38
39// NewPager creates a new Pager with default values.
40func NewPager(tree rotree.IReadOnlyTree, defaultPageSize int, reversed bool) *Pager {
41 return &Pager{
42 Tree: tree,
43 PageQueryParam: "page",
44 SizeQueryParam: "size",
45 DefaultPageSize: defaultPageSize,
46 Reversed: reversed,
47 }
48}
49
50// GetPage retrieves a page of results from the AVL tree.
51func (p *Pager) GetPage(pageNumber int) *Page {
52 return p.GetPageWithSize(pageNumber, p.DefaultPageSize)
53}
54
55func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page {
56 if pageSize <= 0 {
57 panic("GetPageWithSize: invalid page size")
58 }
59
60 totalItems := p.Tree.Size()
61 totalPages := int(math.Ceil(float64(totalItems) / float64(pageSize)))
62
63 page := &Page{
64 TotalItems: totalItems,
65 TotalPages: totalPages,
66 PageSize: pageSize,
67 Pager: p,
68 }
69
70 // page number provided is not available
71 if pageNumber < 1 {
72 page.HasNext = totalPages > 0
73 return page
74 }
75
76 // page number provided is outside the range of total pages
77 if pageNumber > totalPages {
78 page.PageNumber = pageNumber
79 page.HasPrev = pageNumber > 0
80 return page
81 }
82
83 startIndex := (pageNumber - 1) * pageSize
84 endIndex := startIndex + pageSize
85 if endIndex > totalItems {
86 endIndex = totalItems
87 }
88
89 items := []Item{}
90
91 if p.Reversed {
92 p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value any) bool {
93 items = append(items, Item{Key: key, Value: value})
94 return false
95 })
96 } else {
97 p.Tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, value any) bool {
98 items = append(items, Item{Key: key, Value: value})
99 return false
100 })
101 }
102
103 page.Items = items
104 page.PageNumber = pageNumber
105 page.HasPrev = pageNumber > 1
106 page.HasNext = pageNumber < totalPages
107 return page
108}
109
110func (p *Pager) MustGetPageByPath(rawURL string) *Page {
111 page, err := p.GetPageByPath(rawURL)
112 if err != nil {
113 panic("invalid path")
114 }
115 return page
116}
117
118// GetPageByPath retrieves a page of results based on the query parameters in the URL path.
119func (p *Pager) GetPageByPath(rawURL string) (*Page, error) {
120 pageNumber, pageSize, err := p.ParseQuery(rawURL)
121 if err != nil {
122 return nil, err
123 }
124 return p.GetPageWithSize(pageNumber, pageSize), nil
125}
126
127// Picker generates the Markdown UI for the page Picker
128func (p *Page) Picker(path string) string {
129 pageNumber := p.PageNumber
130 pageNumber = max(pageNumber, 1)
131
132 if p.TotalPages <= 1 {
133 return ""
134 }
135
136 u, _ := url.Parse(path)
137 query := u.Query()
138
139 // Remove existing page query parameter
140 query.Del(p.Pager.PageQueryParam)
141
142 // Encode remaining query parameters
143 baseQuery := query.Encode()
144 if baseQuery != "" {
145 baseQuery = "&" + baseQuery
146 }
147 md := ""
148
149 if p.HasPrev {
150 md += ufmt.Sprintf("[%d](?%s=%d%s) | ", 1, p.Pager.PageQueryParam, 1, baseQuery)
151
152 if p.PageNumber > 4 {
153 md += "… | "
154 }
155
156 if p.PageNumber > 3 {
157 md += ufmt.Sprintf("[%d](?%s=%d%s) | ", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2, baseQuery)
158 }
159
160 if p.PageNumber > 2 {
161 md += ufmt.Sprintf("[%d](?%s=%d%s) | ", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1, baseQuery)
162 }
163 }
164
165 if p.PageNumber > 0 && p.PageNumber <= p.TotalPages {
166 md += ufmt.Sprintf("**%d**", p.PageNumber)
167 } else {
168 md += ufmt.Sprintf("_%d_", p.PageNumber)
169 }
170
171 if p.HasNext {
172 if p.PageNumber < p.TotalPages-1 {
173 md += ufmt.Sprintf(" | [%d](?%s=%d%s)", p.PageNumber+1, p.Pager.PageQueryParam, p.PageNumber+1, baseQuery)
174 }
175
176 if p.PageNumber < p.TotalPages-2 {
177 md += ufmt.Sprintf(" | [%d](?%s=%d%s)", p.PageNumber+2, p.Pager.PageQueryParam, p.PageNumber+2, baseQuery)
178 }
179
180 if p.PageNumber < p.TotalPages-3 {
181 md += " | …"
182 }
183
184 md += ufmt.Sprintf(" | [%d](?%s=%d%s)", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages, baseQuery)
185 }
186
187 return md
188}
189
190// ParseQuery parses the URL to extract the page number and page size.
191func (p *Pager) ParseQuery(rawURL string) (int, int, error) {
192 u, err := url.Parse(rawURL)
193 if err != nil {
194 return 1, p.DefaultPageSize, err
195 }
196
197 query := u.Query()
198 pageNumber := 1
199 pageSize := p.DefaultPageSize
200
201 if p.PageQueryParam != "" {
202 if pageStr := query.Get(p.PageQueryParam); pageStr != "" {
203 pageNumber, err = strconv.Atoi(pageStr)
204 if err != nil || pageNumber < 1 {
205 pageNumber = 1
206 }
207 }
208 }
209
210 if p.SizeQueryParam != "" {
211 if sizeStr := query.Get(p.SizeQueryParam); sizeStr != "" {
212 pageSize, err = strconv.Atoi(sizeStr)
213 if err != nil || pageSize < 1 {
214 pageSize = p.DefaultPageSize
215 }
216 }
217 }
218
219 return pageNumber, pageSize, nil
220}
221
222func max(a, b int) int {
223 if a > b {
224 return a
225 }
226 return b
227}