Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}