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

6.24 Kb · 177 lines
  1package home
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"chain/runtime"
  8
  9	"gno.land/p/moul/dynreplacer/v0"
 10	"gno.land/r/moul/config/v1"
 11)
 12
 13// defaultLayout is what renders before a layout slot exists, which is not only
 14// the very first deploy: a redeploy re-runs init() and wipes every slot, so
 15// this is also the page at the exact moment someone is restoring it. It uses
 16// only computed placeholders, so it never shows an unresolved :slug: of its own.
 17//
 18// It prints no gnokey command and no gas numbers, on purpose. The one it used
 19// to print offered 1000000ugnot against 20000000 gas: a ratio of 0.05 against a
 20// mainnet floor of 0.001, and gas_fee is deducted in full as offered and never
 21// refunded, so it asked for about fifty times what the write costs. The tooling
 22// was corrected and this string was not, which is the argument against having
 23// it at all. A command baked into a realm cannot be fixed without a redeploy,
 24// and the redeploy is the thing that wipes the slots. gnohome recomputes it
 25// from content/ and from the chain, so it cannot go stale.
 26const defaultLayout = "# gno.land/r/moul/home\n\n" +
 27	"No layout slot yet. A redeploy wipes every slot and `content/` is the source\n" +
 28	"of truth, so push them back with the tool rather than by hand:\n\n" +
 29	"    go -C tools tool gnohome tx -all\n\n" +
 30	"No gas numbers here on purpose: a command written into a realm goes stale and\n" +
 31	"cannot be corrected without another redeploy.\n\n" +
 32	"## Slots\n\n:slots:\n\n---\n\nrev :rev: · height :height: · :chainid:\n"
 33
 34func chainHeight() int64 { return runtime.ChainHeight() }
 35
 36// Render serves three views:
 37//
 38//	""            the assembled page, what https://gno.land/u/moul shows
 39//	"slots"       the slot index: name, size, revision, last write
 40//	"slots/<slug>" one slot's raw markdown, fenced
 41func Render(path string) string {
 42	switch {
 43	case path == "":
 44		return renderPage()
 45	case path == "slots":
 46		return renderIndex()
 47	case strings.HasPrefix(path, "slots/"):
 48		return renderSlot(strings.TrimPrefix(path, "slots/"))
 49	default:
 50		return "# Not found\n\nNo such path: " + strconv.Quote(path) +
 51			"\n\nTry [the slot index](/r/moul/home:slots).\n"
 52	}
 53}
 54
 55// renderPage fills the layout. dynreplacer only invokes the callbacks whose
 56// placeholder actually occurs in the layout, so an unused slot is never even
 57// read out of the tree.
 58func renderPage() string {
 59	layout := Get(layoutSlug)
 60	if layout == "" {
 61		layout = defaultLayout
 62	}
 63
 64	r := dynreplacer.New()
 65
 66	// Computed from chain state. Registered first; assertWritableSlug keeps a
 67	// slot from ever taking one of these names, so nothing shadows them.
 68	r.RegisterCallback(":owner:", func() string { return admin.String() })
 69	r.RegisterCallback(":realm:", func() string { return realmPath })
 70	r.RegisterCallback(":chainid:", func() string { return runtime.ChainID() })
 71	r.RegisterCallback(":height:", func() string { return strconv.FormatInt(runtime.ChainHeight(), 10) })
 72	r.RegisterCallback(":rev:", func() string { return strconv.Itoa(rev) })
 73	r.RegisterCallback(":slots:", func() string { return slotLinks() })
 74
 75	// The explorer links, which are computed the same way but come from
 76	// another realm's configuration. See scan.gno.
 77	registerScanCallbacks(r)
 78
 79	// The reaction block, which is the one thing on this page a reader can
 80	// act on. See reactions.gno.
 81	registerReactionCallbacks(r)
 82
 83	// One callback per slot. The closure captures s, which is re-bound on each
 84	// iteration, so every callback sees its own slot.
 85	slots.Iterate("", "", func(key string, value any) bool {
 86		s := value.(*slot)
 87		r.RegisterCallback(":"+key+":", func() string { return s.body })
 88		return false
 89	})
 90
 91	// The notice blocks, from gno.land/r/moul/config: a warning, a changelog
 92	// line or a bit of news, set either on every realm of moul's at once or on
 93	// this one alone, and empty by default. A pause banner rides along at the
 94	// top, so the page explains itself if this realm is ever held back.
 95	//
 96	// The path is passed explicitly rather than using config's zero-argument
 97	// TopBlock(). That form reads the caller off the stack, which is correct
 98	// here and is covered by config's own tests, but this realm's Render IS
 99	// gno.land/u/moul and it already states its own path: passing the constant
100	// cannot be wrong, at no cost.
101	return config.TopBlockFor(realmPath) + r.Replace(layout) + config.BottomBlockFor(realmPath)
102}
103
104// slotLinks is the :slots: placeholder: a bullet list of every slot, linking to
105// its raw view.
106func slotLinks() string {
107	if slots.Size() == 0 {
108		return "_no slots yet_"
109	}
110	var b strings.Builder
111	slots.Iterate("", "", func(key string, value any) bool {
112		b.WriteString("- [")
113		b.WriteString(key)
114		b.WriteString("](/r/moul/home:slots/")
115		b.WriteString(key)
116		b.WriteString(")\n")
117		return false
118	})
119	return strings.TrimSuffix(b.String(), "\n")
120}
121
122func renderIndex() string {
123	var b strings.Builder
124	b.WriteString("# Slots\n\n")
125	b.WriteString("rev ")
126	b.WriteString(strconv.Itoa(rev))
127	b.WriteString(" · ")
128	b.WriteString(strconv.Itoa(slots.Size()))
129	b.WriteString(" slot(s)\n\n")
130	if slots.Size() == 0 {
131		b.WriteString("_no slots yet_\n")
132		return b.String()
133	}
134	b.WriteString("| slot | bytes | rev | height |\n")
135	b.WriteString("| --- | ---: | ---: | ---: |\n")
136	slots.Iterate("", "", func(key string, value any) bool {
137		s := value.(*slot)
138		b.WriteString("| [")
139		b.WriteString(key)
140		b.WriteString("](/r/moul/home:slots/")
141		b.WriteString(key)
142		b.WriteString(") | ")
143		b.WriteString(strconv.Itoa(len(s.body)))
144		b.WriteString(" | ")
145		b.WriteString(strconv.Itoa(s.rev))
146		b.WriteString(" | ")
147		b.WriteString(strconv.FormatInt(s.updated, 10))
148		b.WriteString(" |\n")
149		return false
150	})
151	return b.String()
152}
153
154func renderSlot(slug string) string {
155	v := slots.Get(slug)
156	if v == nil {
157		return "# Not found\n\nNo slot named " + strconv.Quote(slug) +
158			".\n\nTry [the slot index](/r/moul/home:slots).\n"
159	}
160	s := v.(*slot)
161	var b strings.Builder
162	b.WriteString("# ")
163	b.WriteString(slug)
164	b.WriteString("\n\n")
165	b.WriteString(strconv.Itoa(len(s.body)))
166	b.WriteString(" bytes · rev ")
167	b.WriteString(strconv.Itoa(s.rev))
168	b.WriteString(" · written at height ")
169	b.WriteString(strconv.FormatInt(s.updated, 10))
170	b.WriteString("\n\n```\n")
171	b.WriteString(s.body)
172	if !strings.HasSuffix(s.body, "\n") {
173		b.WriteString("\n")
174	}
175	b.WriteString("```\n")
176	return b.String()
177}