1// Package pageable implements a flexible pagination system that can be used with any data structure2// that implements the Pageable interface. It provides functionality for:3//4// - Paginating through collections of items5// - Configurable page sizes6// - Forward and reverse iteration7// - URL query parameter parsing8// - Markdown-based pagination UI9//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 ulist13package pageable1415import (16 "math"17 "net/url"18 "strconv"1920 "gno.land/p/nt/ufmt/v0"21)2223// 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 forward26// and reverse iteration, this interface consolidates both directions into a single27// IterateByOffset method. It's the responsibility of the implementing wrapper to28// handle the direction logic internally based on the offset and count parameters.29type Pageable interface {30 // Size returns the total number of items31 Size() int3233 // 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 internally37 // 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 supporting42 // bidirectional iteration through the implementation layer.43 IterateByOffset(offset int, count int, cb func(index interface{}, value interface{}) bool) bool44}4546// Pager provides pagination functionality for any Pageable source47type Pager struct {48 Source Pageable49 PageQueryParam string50 SizeQueryParam string51 DefaultPageSize int52 Reversed bool53}5455// Page represents a single page of results56type Page struct {57 Items []Item58 PageNumber int59 PageSize int60 TotalItems int61 TotalPages int62 HasPrev bool63 HasNext bool64 Pager *Pager65}6667// Item represents a generic item in the page68type Item struct {69 Index interface{}70 Value interface{}71}7273// NewPager creates a new Pager with default values74func 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}8384// 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}8889// Helper function to create a basic page90func (p *Pager) newBasePage(pageSize int) *Page {91 totalItems := p.Source.Size()92 totalPages := 093 if pageSize > 0 {94 totalPages = int(math.Ceil(float64(totalItems) / float64(pageSize)))95 }9697 return &Page{98 TotalItems: totalItems,99 TotalPages: totalPages,100 PageSize: pageSize,101 Pager: p,102 }103}104105func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page {106 // Handle invalid page size107 if pageSize <= 0 {108 return p.newBasePage(pageSize)109 }110111 page := p.newBasePage(pageSize)112113 // For empty source, return empty page with no navigation114 if page.TotalItems == 0 {115 page.HasPrev = false116 page.HasNext = false117 return page118 }119120 // page number provided is not available121 if pageNumber < 1 {122 page.HasNext = page.TotalPages > 0123 return page124 }125126 // page number provided is outside the range of total pages127 if pageNumber > page.TotalPages {128 page.PageNumber = pageNumber129 page.HasPrev = page.TotalPages > 0130 return page131 }132133 // Calculate offset and size134 var offset, iterCount int135 if p.Reversed {136 // For reversed order, calculate from the end137 iterCount = min(pageSize, page.TotalItems-((pageNumber-1)*pageSize))138 offset = max(0, page.TotalItems-(pageNumber*pageSize))139 } else {140 offset = (pageNumber - 1) * pageSize141 iterCount = pageSize142 }143144 // Collect items for the current page145 var items []Item146 if p.Reversed {147 // For reversed order, use negative count to indicate reverse iteration148 offset = page.TotalItems - offset - iterCount149 p.Source.IterateByOffset(offset, -iterCount, func(index interface{}, value interface{}) bool {150 items = append(items, Item{Index: index, Value: value})151 return false152 })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 false157 })158 }159160 page.Items = items161 page.PageNumber = pageNumber162 page.HasPrev = pageNumber > 1163 page.HasNext = pageNumber < page.TotalPages164 return page165}166167func (p *Pager) MustGetPageByPath(rawURL string) *Page {168 page, err := p.GetPageByPath(rawURL)169 if err != nil {170 panic("invalid path")171 }172 return page173}174175// 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, err180 }181 return p.GetPageWithSize(pageNumber, pageSize), nil182}183184// Picker generates the Markdown UI for the page picker using default query string format185func (p *Page) Picker() string {186 return p.PickerWithPath("")187}188189// PickerWithPath generates the Markdown UI for the page picker with optional path190// If path is provided, it preserves existing query parameters (except page)191func (p *Page) PickerWithPath(path string) string {192 pageNumber := p.PageNumber193 pageNumber = max(pageNumber, 1)194195 if p.TotalPages <= 1 {196 return ""197 }198199 // Parse existing query parameters if path is provided200 baseQuery := ""201 if path != "" {202 u, _ := url.Parse(path)203 if u != nil {204 query := u.Query()205 // Remove existing page query parameter206 query.Del(p.Pager.PageQueryParam)207 // Encode remaining query parameters208 baseQuery = query.Encode()209 if baseQuery != "" {210 baseQuery = "&" + baseQuery211 }212 }213 }214215 md := ""216217 if p.HasPrev {218 // Always show the first page link219 md += ufmt.Sprintf("[%d](?%s=%d%s) | ", 1, p.Pager.PageQueryParam, 1, baseQuery)220221 // Before222 if p.PageNumber > 4 {223 md += "… | "224 }225226 if p.PageNumber > 3 {227 md += ufmt.Sprintf("[%d](?%s=%d%s) | ", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2, baseQuery)228 }229230 if p.PageNumber > 2 {231 md += ufmt.Sprintf("[%d](?%s=%d%s) | ", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1, baseQuery)232 }233 }234235 if p.PageNumber > 0 && p.PageNumber <= p.TotalPages {236 // Current page237 md += ufmt.Sprintf("**%d**", p.PageNumber)238 } else {239 md += ufmt.Sprintf("_%d_", p.PageNumber)240 }241242 if p.HasNext {243 md += " | "244245 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 }248249 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 }252253 if p.PageNumber < p.TotalPages-3 {254 md += "… | "255 }256257 // Always show the last page link258 md += ufmt.Sprintf("[%d](?%s=%d%s)", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages, baseQuery)259 }260261 return md262}263264// Helper to safely parse positive integers with a default value265func parsePositiveIntOrDefault(str string, defaultValue int) int {266 if str == "" {267 return defaultValue268 }269 val, err := strconv.Atoi(str)270 if err != nil || val < 1 {271 return defaultValue272 }273 return val274}275276func (p *Pager) ParseQuery(rawURL string) (int, int, error) {277 u, err := url.Parse(rawURL)278 if err != nil {279 return 1, p.DefaultPageSize, err280 }281282 query := u.Query()283 pageNumber := parsePositiveIntOrDefault(query.Get(p.PageQueryParam), 1)284 pageSize := parsePositiveIntOrDefault(query.Get(p.SizeQueryParam), p.DefaultPageSize)285286 return pageNumber, pageSize, nil287}288289func max(a, b int) int {290 if a > b {291 return a292 }293 return b294}295296func min(a, b int) int {297 if a < b {298 return a299 }300 return b301}302Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.