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}