render.gno
4.71 Kb · 181 lines
1package home
2
3import (
4 "strconv"
5 "strings"
6
7 "chain/runtime"
8)
9
10const (
11 realmPath = "gno.land/r/g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c/home"
12 webPath = "/r/g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c/home"
13)
14
15// fallbackLayout renders when the layout slot is deleted.
16const fallbackLayout = "# zôÖma\n\n:bio:\n\n:hearts:\n"
17
18// Render serves:
19//
20// "" the profile page
21// "slots" the slot index
22// "slots/<slug>" one slot's raw markdown
23// "hearts" everyone who sent a <3
24func Render(path string) string {
25 switch {
26 case path == "":
27 return renderPage()
28 case path == "slots":
29 return renderIndex()
30 case strings.HasPrefix(path, "slots/"):
31 return renderSlot(strings.TrimPrefix(path, "slots/"))
32 case path == "hearts":
33 return renderHearts()
34 }
35 return "# Not found\n\nNo such path: " + strconv.Quote(path) + "\n\n[Back home](" + webPath + ")\n"
36}
37
38func renderPage() string {
39 layout := Get(layoutSlug)
40 if layout == "" {
41 layout = fallbackLayout
42 }
43 return fill(layout)
44}
45
46// fill replaces every :slug: in s, in one left-to-right pass. Replacement text
47// is never rescanned, so a slot cannot expand into another slot. A placeholder
48// with no value stays verbatim: a missing section should be visible.
49func fill(s string) string {
50 var b strings.Builder
51 for {
52 i := strings.IndexByte(s, ':')
53 if i < 0 {
54 b.WriteString(s)
55 return b.String()
56 }
57 b.WriteString(s[:i])
58 s = s[i:]
59 j := strings.IndexByte(s[1:], ':')
60 if j < 0 {
61 b.WriteString(s)
62 return b.String()
63 }
64 slug := s[1 : j+1]
65 if v, ok := lookup(slug); ok {
66 b.WriteString(v)
67 s = s[j+2:]
68 continue
69 }
70 // Not a placeholder: emit the first ':' and rescan from the next one,
71 // so "a:b :bio:" still finds :bio:.
72 b.WriteString(":")
73 s = s[1:]
74 }
75}
76
77func lookup(slug string) (string, bool) {
78 if !validSlug(slug) {
79 return "", false
80 }
81 switch slug {
82 case "chainid":
83 return runtime.ChainID(), true
84 case "height":
85 return strconv.FormatInt(runtime.ChainHeight(), 10), true
86 case "hearts":
87 return heartsBlock(), true
88 case "hero":
89 return heroImage(), true
90 case "realm":
91 return realmPath, true
92 case "rev":
93 return strconv.Itoa(rev), true
94 }
95 if strings.HasPrefix(slug, galleryPrefix) {
96 return renderGallery(strings.TrimPrefix(slug, galleryPrefix))
97 }
98 if v := slots.Get(slug); v != nil {
99 return v.(*slot).body, true
100 }
101 return "", false
102}
103
104func heartsBlock() string {
105 n := hearts.Size()
106 var b strings.Builder
107 b.WriteString("**")
108 b.WriteString(strconv.Itoa(n))
109 if n == 1 {
110 b.WriteString("** heart so far.")
111 } else {
112 b.WriteString("** hearts so far.")
113 }
114 b.WriteString(" [Send yours](" + webPath + "$help&func=Heart): one transaction, one per address, forever on chain.")
115 if len(recent) > 0 {
116 b.WriteString("\n\nLatest: ")
117 for i, a := range recent {
118 if i > 0 {
119 b.WriteString(" · ")
120 }
121 b.WriteString(userLink(a))
122 }
123 b.WriteString(" · [all](" + webPath + ":hearts)")
124 }
125 return b.String()
126}
127
128// userLink renders an address as a short link to its gno.land profile.
129func userLink(a address) string {
130 s := a.String()
131 short := s
132 if len(s) > 12 {
133 short = s[:7] + "…" + s[len(s)-4:]
134 }
135 return "[" + short + "](/u/" + s + ")"
136}
137
138func renderHearts() string {
139 var b strings.Builder
140 b.WriteString("# <3\n\n")
141 b.WriteString(strconv.Itoa(hearts.Size()) + " address(es) sent a heart. [Send yours](" + webPath + "$help&func=Heart).\n\n")
142 if hearts.Size() == 0 {
143 b.WriteString("_Be the first._\n")
144 return b.String()
145 }
146 b.WriteString("| from | block |\n| --- | ---: |\n")
147 hearts.Iterate("", "", func(key string, value any) bool {
148 b.WriteString("| " + userLink(address(key)) + " | " + strconv.FormatInt(value.(int64), 10) + " |\n")
149 return false
150 })
151 b.WriteString("\n[Back home](" + webPath + ")\n")
152 return b.String()
153}
154
155func renderIndex() string {
156 var b strings.Builder
157 b.WriteString("# Slots\n\nrev " + strconv.Itoa(rev) + " · " + strconv.Itoa(slots.Size()) + " slot(s)\n\n")
158 b.WriteString("| slot | bytes | rev | height |\n| --- | ---: | ---: | ---: |\n")
159 slots.Iterate("", "", func(key string, value any) bool {
160 s := value.(*slot)
161 b.WriteString("| [" + key + "](" + webPath + ":slots/" + key + ") | " +
162 strconv.Itoa(len(s.body)) + " | " + strconv.Itoa(s.rev) + " | " +
163 strconv.FormatInt(s.updated, 10) + " |\n")
164 return false
165 })
166 return b.String()
167}
168
169func renderSlot(slug string) string {
170 v := slots.Get(slug)
171 if v == nil {
172 return "# Not found\n\nNo slot named " + strconv.Quote(slug) + ".\n\n[Slot index](" + webPath + ":slots)\n"
173 }
174 s := v.(*slot)
175 body := s.body
176 if !strings.HasSuffix(body, "\n") {
177 body += "\n"
178 }
179 return "# " + slug + "\n\n" + strconv.Itoa(len(s.body)) + " bytes · rev " + strconv.Itoa(s.rev) +
180 " · written at height " + strconv.FormatInt(s.updated, 10) + "\n\n````\n" + body + "````\n"
181}