render.gno
4.56 Kb · 148 lines
1package home
2
3import (
4 "strconv"
5 "strings"
6
7 "chain/runtime"
8
9 "gno.land/p/moul/dynreplacer/v0"
10)
11
12// defaultLayout is what renders before a layout slot exists, i.e. right after
13// the very first deploy. It uses only computed placeholders, so it never shows
14// an unresolved :slug: of its own.
15const defaultLayout = "# gno.land/r/moul/home\n\n" +
16 "No layout slot yet. Set one, then the page is whatever you make it:\n\n" +
17 " gnokey maketx call -pkgpath gno.land/r/moul/home -func Set \\\n" +
18 " -args layout -args \"$(/bin/cat content/layout.md)\" \\\n" +
19 " -gas-fee 1000000ugnot -gas-wanted 20000000 \\\n" +
20 " -broadcast -chainid gnoland-1 -remote https://rpc.gno.land:443 moul\n\n" +
21 "## Slots\n\n:slots:\n\n---\n\nrev :rev: · height :height: · :chainid:\n"
22
23func chainHeight() int64 { return runtime.ChainHeight() }
24
25// Render serves three views:
26//
27// "" the assembled page, what https://gno.land/u/moul shows
28// "slots" the slot index: name, size, revision, last write
29// "slots/<slug>" one slot's raw markdown, fenced
30func Render(path string) string {
31 switch {
32 case path == "":
33 return renderPage()
34 case path == "slots":
35 return renderIndex()
36 case strings.HasPrefix(path, "slots/"):
37 return renderSlot(strings.TrimPrefix(path, "slots/"))
38 default:
39 return "# Not found\n\nNo such path: " + strconv.Quote(path) +
40 "\n\nTry [the slot index](/r/moul/home:slots).\n"
41 }
42}
43
44// renderPage fills the layout. dynreplacer only invokes the callbacks whose
45// placeholder actually occurs in the layout, so an unused slot is never even
46// read out of the tree.
47func renderPage() string {
48 layout := Get(layoutSlug)
49 if layout == "" {
50 layout = defaultLayout
51 }
52
53 r := dynreplacer.New()
54
55 // Computed from chain state. Registered first; assertWritableSlug keeps a
56 // slot from ever taking one of these names, so nothing shadows them.
57 r.RegisterCallback(":owner:", func() string { return admin.String() })
58 r.RegisterCallback(":realm:", func() string { return "gno.land/r/moul/home" })
59 r.RegisterCallback(":chainid:", func() string { return runtime.ChainID() })
60 r.RegisterCallback(":height:", func() string { return strconv.FormatInt(runtime.ChainHeight(), 10) })
61 r.RegisterCallback(":rev:", func() string { return strconv.Itoa(rev) })
62 r.RegisterCallback(":slots:", func() string { return slotLinks() })
63
64 // One callback per slot. The closure captures s, which is re-bound on each
65 // iteration, so every callback sees its own slot.
66 slots.Iterate("", "", func(key string, value any) bool {
67 s := value.(*slot)
68 r.RegisterCallback(":"+key+":", func() string { return s.body })
69 return false
70 })
71
72 return r.Replace(layout)
73}
74
75// slotLinks is the :slots: placeholder: a bullet list of every slot, linking to
76// its raw view.
77func slotLinks() string {
78 if slots.Size() == 0 {
79 return "_no slots yet_"
80 }
81 var b strings.Builder
82 slots.Iterate("", "", func(key string, value any) bool {
83 b.WriteString("- [")
84 b.WriteString(key)
85 b.WriteString("](/r/moul/home:slots/")
86 b.WriteString(key)
87 b.WriteString(")\n")
88 return false
89 })
90 return strings.TrimSuffix(b.String(), "\n")
91}
92
93func renderIndex() string {
94 var b strings.Builder
95 b.WriteString("# Slots\n\n")
96 b.WriteString("rev ")
97 b.WriteString(strconv.Itoa(rev))
98 b.WriteString(" · ")
99 b.WriteString(strconv.Itoa(slots.Size()))
100 b.WriteString(" slot(s)\n\n")
101 if slots.Size() == 0 {
102 b.WriteString("_no slots yet_\n")
103 return b.String()
104 }
105 b.WriteString("| slot | bytes | rev | height |\n")
106 b.WriteString("| --- | ---: | ---: | ---: |\n")
107 slots.Iterate("", "", func(key string, value any) bool {
108 s := value.(*slot)
109 b.WriteString("| [")
110 b.WriteString(key)
111 b.WriteString("](/r/moul/home:slots/")
112 b.WriteString(key)
113 b.WriteString(") | ")
114 b.WriteString(strconv.Itoa(len(s.body)))
115 b.WriteString(" | ")
116 b.WriteString(strconv.Itoa(s.rev))
117 b.WriteString(" | ")
118 b.WriteString(strconv.FormatInt(s.updated, 10))
119 b.WriteString(" |\n")
120 return false
121 })
122 return b.String()
123}
124
125func renderSlot(slug string) string {
126 v := slots.Get(slug)
127 if v == nil {
128 return "# Not found\n\nNo slot named " + strconv.Quote(slug) +
129 ".\n\nTry [the slot index](/r/moul/home:slots).\n"
130 }
131 s := v.(*slot)
132 var b strings.Builder
133 b.WriteString("# ")
134 b.WriteString(slug)
135 b.WriteString("\n\n")
136 b.WriteString(strconv.Itoa(len(s.body)))
137 b.WriteString(" bytes · rev ")
138 b.WriteString(strconv.Itoa(s.rev))
139 b.WriteString(" · written at height ")
140 b.WriteString(strconv.FormatInt(s.updated, 10))
141 b.WriteString("\n\n```\n")
142 b.WriteString(s.body)
143 if !strings.HasSuffix(s.body, "\n") {
144 b.WriteString("\n")
145 }
146 b.WriteString("```\n")
147 return b.String()
148}