urlfilter.gno
4.11 Kb · 137 lines
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 filter
3// and each value is an avl.Tree containing items associated with that filter.
4package urlfilter
5
6import (
7 "net/url"
8 "strings"
9
10 "gno.land/p/nt/avl"
11 "gno.land/p/nt/ufmt"
12 "gno.land/p/sunspirit/md"
13)
14
15// ApplyFilters filters items based on the "filter" query parameter in the given URL
16// and generates a Markdown representation of all available filters.
17//
18// Expected `items` structure:
19// - `items` is an *avl.Tree where each key is a filter name (e.g., "T1", "size:XL", "on_sale")
20// and each value is an *avl.Tree containing the items for that filter.
21// - Each item tree uses:
22// Key (string): Unique item identifier
23// Value (any) : Optional associated item data
24//
25// Example:
26//
27// // Build the main filters tree
28// filters := avl.NewTree()
29//
30// // Subtree for filter "T1"
31// t1Items := avl.NewTree()
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 filters
42// // items → AVL tree containing the filtered items
43func ApplyFilters(u *url.URL, items *avl.Tree, paramName string) (string, *avl.Tree) {
44 active := parseFilterMap(u.Query(), paramName)
45 allFilters := make([]string, 0)
46 resultTree := avl.NewTree()
47
48 // Iterate over each filter group in the items tree
49 items.Iterate("", "", func(filterKey string, subtree interface{}) bool {
50 allFilters = append(allFilters, filterKey)
51
52 tree, ok := subtree.(*avl.Tree)
53 if !ok {
54 return false
55 }
56
57 // Add items to result if there are no active filters
58 // or if the current filter is active
59 tree.Iterate("", "", func(itemKey string, _ interface{}) bool {
60 if len(active) == 0 || active[filterKey] {
61 resultTree.Set(itemKey, filterKey)
62 }
63 return false
64 })
65 return false
66 })
67
68 // Build Markdown links for toggling each filter
69 var sb strings.Builder
70 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 }
75
76 return sb.String(), resultTree
77}
78
79// 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 + "?" + enc
83 }
84 return path
85}
86
87// parseFilterMap reads the "filter" query parameter and converts it into a map
88// 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] = true
102 }
103 }
104 return m
105}
106
107// 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 inactive
116 newFilters = append(newFilters, f)
117 }
118 } else if active[f] { // Keep other active filters
119 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 q
127}
128
129// formatLabel returns the Markdown-formatted label for a filter,
130// showing active filters in bold (**filter**) and inactive filters
131// 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}