render.gno
1.77 Kb · 59 lines
1package memba_points_v1
2
3import (
4 "strings"
5
6 "gno.land/p/nt/ufmt/v0"
7)
8
9// Render — human gnoweb view. "" → home (totals + owner + awarders); "a/<addr>" → one balance.
10func Render(path string) string {
11 if path == "" {
12 return ufmt.Sprintf(
13 "# Memba Points (MP)\n\nSoulbound, non-transferable reputation points.\n\n"+
14 "- Holders: **%d**\n- Total points: **%d**\n- Paused: **%t**\n- Owner: `%s`\n- Awarders: %s\n",
15 holderCount, totalPoints, paused, owner.String(), renderAwarders())
16 }
17 if strings.HasPrefix(path, "a/") {
18 addr := strings.TrimPrefix(path, "a/")
19 pts := GetPoints(addr) // raw addr for the lookup; the displayed value is sanitized
20 return ufmt.Sprintf("# Points for `%s`\n\n**%d** MP · Tier: %s\n",
21 mdInlineEscape(addr), pts, mdInlineEscape(tierFor(pts)))
22 }
23 return "# 404\n"
24}
25
26// mdInlineEscape drops markdown-significant characters from path-derived / untrusted values before
27// writing them into Render output, preventing markdown/code-span injection (gno contract-review #10).
28// Well-formed bech32 addresses and tier names contain none of these, so this is loss-free in practice.
29func mdInlineEscape(s string) string {
30 var sb strings.Builder
31 for _, r := range s {
32 switch r {
33 case '`', '|', '<', '>', '\n', '\r', '\t', '*', '_', '[', ']', '#':
34 // drop markdown-significant runes
35 default:
36 sb.WriteString(string(r))
37 }
38 }
39 return sb.String()
40}
41
42func renderAwarders() string {
43 var sb strings.Builder
44 first := true
45 awarders.Iterate("", "", func(k string, _ any) bool {
46 if !first {
47 sb.WriteString(", ")
48 }
49 first = false
50 sb.WriteString("`")
51 sb.WriteString(mdInlineEscape(k)) // awarder pkgpath is owner-set but still escaped (checklist #10)
52 sb.WriteString("`")
53 return false
54 })
55 if first {
56 return "_none_"
57 }
58 return sb.String()
59}