Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}