1package tablesort23import (4 "net/url"5 "strings"67 "gno.land/p/mason/md/v0"8 "gno.land/p/nt/ufmt/v0"9)1011// Table holds the headings and rows for rendering.12// Each row must have the same number of cells as there are headings.13type Table struct {14 Headings []string // ["A", "B", "C"]15 Rows [][]string // [["a1","b1","c1"], ["a2","b2","c2"], ...]16}1718// Render generates a Markdown table from a Table struct with sortable columns based on URL params.19// paramPrefix is an optional prefix for in the URL to identify the tablesort Renders (e.g. "members-").20func Render(u *url.URL, table *Table, paramPrefix string) string {21 direction := ""22 currentHeading := ""23 if h := u.Query().Get(paramPrefix + "sort-asc"); h != "" {24 direction = "asc"25 currentHeading = h26 } else if h := u.Query().Get(paramPrefix + "sort-desc"); h != "" {27 direction = "desc"28 currentHeading = h29 }3031 var sb strings.Builder3233 // Find the index of the column to sort34 colIndex := -135 for i, h := range table.Headings {36 if h == currentHeading {37 colIndex = i38 break39 }40 }4142 // Sort rows if necessary43 if colIndex != -1 {44 SortRows(table.Rows, colIndex, direction == "asc")45 }4647 // Build header48 sb.WriteString(buildHeader(u, table.Headings, currentHeading, direction, paramPrefix))49 sb.WriteString("\n")5051 numCols := len(table.Headings)5253 // Build rows54 for i, row := range table.Rows {55 // Validate row length56 if len(row) != numCols {57 return "tablesort fails: row " + ufmt.Sprintf("%d", i+1) + " has " +58 ufmt.Sprintf("%d", len(row)) + " cells, expected " +59 ufmt.Sprintf("%d", numCols) + ", because there are " + ufmt.Sprintf("%d", numCols) + " columns.\n"60 }6162 sb.WriteString("|")63 for _, cell := range row {64 sb.WriteString(" " + cell + " |")65 }66 sb.WriteString("\n")67 }6869 return sb.String()70}7172// buildHeader builds the Markdown header row with clickable links and arrows73func buildHeader(u *url.URL, headings []string, currentHeading, direction string, paramPrefix string) string {74 var sb strings.Builder75 sb.WriteString("|")76 for _, h := range headings {77 arrow := ""78 if h == currentHeading {79 if direction == "asc" {80 arrow = " ↑"81 } else if direction == "desc" {82 arrow = " ↓"83 }84 }8586 // Build URL for the header link with toggle logic87 newURL := *u88 q := newURL.Query()89 if h == currentHeading {90 // Toggle sort direction91 if direction == "asc" {92 q.Del(paramPrefix + "sort-asc")93 q.Set(paramPrefix+"sort-desc", h)94 } else {95 q.Del(paramPrefix + "sort-desc")96 q.Set(paramPrefix+"sort-asc", h)97 }98 } else {99 // First click defaults to descending100 q.Del(paramPrefix + "sort-asc")101 q.Set(paramPrefix+"sort-desc", h)102 }103 newURL.RawQuery = q.Encode()104 link := md.Link(h+arrow, newURL.String())105 sb.WriteString(" " + link + " |")106 }107108 sb.WriteString("\n|")109 for range headings {110 sb.WriteString(" --- |")111 }112 return sb.String()113}114Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.