1// Package urlfilter provides functionality to filter items based on URL query parameters.2// It is designed to work with an avl.Tree structure where each key represents a filter3// and each value is an avl.Tree containing items associated with that filter.4package urlfilter56import (7 "net/url"8 "strings"910 "gno.land/p/nt/bptree/v0"11 "gno.land/p/nt/ufmt/v0"12 "gno.land/p/sunspirit/md/v0"13)1415// ApplyFilters filters items based on the "filter" query parameter in the given URL16// and generates a Markdown representation of all available filters.17//18// Expected `items` structure:19// - `items` is an *bptree.BPTree where each key is a filter name (e.g., "T1", "size:XL", "on_sale")20// and each value is an *bptree.BPTree containing the items for that filter.21// - Each item tree uses:22// Key (string): Unique item identifier23// Value (any) : Optional associated item data24//25// Example:26//27// // Build the main filters tree28// filters := bptree.NewBPTree32()29//30// // Subtree for filter "T1"31// t1Items := bptree.NewBPTree32()32// t1Items.Set("item1", nil)33// t1Items.Set("item2", nil)34// filters.Set("T1", t1Items)35//36// // URL with active filter "T1"37// u, _ := url.Parse("/shop?filter=T1")38//39// mdFilters, items := ApplyFilters(u, filters, "filter")40//41// // mdFilters → Markdown links for toggling filters42// // items → AVL tree containing the filtered items43func ApplyFilters(u *url.URL, items *bptree.BPTree, paramName string) (string, *bptree.BPTree) {44 active := parseFilterMap(u.Query(), paramName)45 allFilters := make([]string, 0)46 resultTree := bptree.NewBPTree32()4748 // Iterate over each filter group in the items tree49 items.Iterate("", "", func(filterKey string, subtree interface{}) bool {50 allFilters = append(allFilters, filterKey)5152 tree, ok := subtree.(*bptree.BPTree)53 if !ok {54 return false55 }5657 // Add items to result if there are no active filters58 // or if the current filter is active59 tree.Iterate("", "", func(itemKey string, _ interface{}) bool {60 if len(active) == 0 || active[filterKey] {61 resultTree.Set(itemKey, filterKey)62 }63 return false64 })65 return false66 })6768 // Build Markdown links for toggling each filter69 var sb strings.Builder70 for _, f := range allFilters {71 q := toggleFilterQuery(active, f, allFilters, paramName)72 urlStr := buildURL(u.Path, q)73 sb.WriteString(ufmt.Sprintf(" | %v ", md.Link(formatLabel(f, active[f]), urlStr)))74 }7576 return sb.String(), resultTree77}7879// buildURL returns a path + query string, omitting the "?" if no query exists.80func buildURL(path string, query url.Values) string {81 if enc := query.Encode(); enc != "" {82 return path + "?" + enc83 }84 return path85}8687// parseFilterMap reads the "filter" query parameter and converts it into a map88// where keys are filter names and values are true for active filters.89//90// Example:91//92// "filter=T1,T2" -> map[string]bool{"T1": true, "T2": true}93func parseFilterMap(query url.Values, paramName string) map[string]bool {94 filterStr := strings.TrimSpace(query.Get(paramName))95 if filterStr == "" {96 return map[string]bool{}97 }98 m := make(map[string]bool)99 for _, f := range strings.Split(filterStr, ",") {100 if f = strings.TrimSpace(f); f != "" {101 m[f] = true102 }103 }104 return m105}106107// toggleFilterQuery returns a new query string with the given filter toggled.108// - If the filter is currently active, it will be removed.109// - If it is inactive, it will be added.110// The order of filters follows the `all` list for consistency.111func toggleFilterQuery(active map[string]bool, toggled string, all []string, paramName string) url.Values {112 newFilters := []string{}113 for _, f := range all {114 if f == toggled {115 if !active[f] { // Add if it was inactive116 newFilters = append(newFilters, f)117 }118 } else if active[f] { // Keep other active filters119 newFilters = append(newFilters, f)120 }121 }122 q := url.Values{}123 if len(newFilters) > 0 {124 q.Set(paramName, strings.Join(newFilters, ","))125 }126 return q127}128129// formatLabel returns the Markdown-formatted label for a filter,130// showing active filters in bold (**filter**) and inactive filters131// with strikethrough (~~filter~~).132func formatLabel(name string, active bool) string {133 if active {134 return md.Bold(name)135 }136 return md.Strikethrough(name)137}138Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.