Kind Realm (renderable)
Name home
Namespace g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm
Exported functions 30
Module gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home
gno 0.9 render.gno gno
⧉
1 package home 2 3 import ( 4 "strconv" 5 "strings" 6 7 "chain/runtime" 8 Accept (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, pkgPath string)
Append (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string, body string)
Authority () string
Delete (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string)
Each (fn func(string, gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home.Slot) bool)
Fallback (path string) string
Forget (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Freeze (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Frozen () bool
Get (slug string) string
Has (slug string) bool
HistoryPaths () []string
LastHeight () int64
Layout () string
Link (sub string) string
LivePath () string
Lookup (slug string) (struct{Body string; Rev int; Height int64}, bool)
Manifest () string
NewFiller () *gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home.Filler
Path () string
PendingPaths () []string
Register (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, t interface {Render func(string) string})
Render (path string) string
Revision () int
Rollback (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Set (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string, body string)
Slugs () []string
Style (key string, fallback string) string
TransferAuthority (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, to string)
Withdraw (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, pkgPath string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
)
9
10 // Render is what gnoweb calls.
11 //
12 // The operator views (system, slots, edit, manifest) are always served here,
13 // never through the theme: a panic inside a foreign realm's code aborts the
14 // whole call and cannot be recovered from this side, so a theme that panics
15 // takes the page down until it is rolled back. These views are what the
16 // rollback is done from, and the routing keeps them out of the theme's reach.
17 // Every other path, the page itself included, goes to the live theme, or to
18 // the built-in renderer when none is accepted.
19 func Render(path string ) string {
20 if isOperatorPath(path) {
21 return Fallback(path)
22 }
23 if t, ok := liveTheme(); ok {
24 return t.Render(path)
25 }
26 return Fallback(path)
27 }
28
29 // operatorPaths are the render paths this realm keeps for itself.
30 var operatorPaths = [] string { "system" , "slots" , "edit" , "manifest" }
31
32 func isOperatorPath(path string ) bool {
33 path = cleanPath(path)
34 for _, p := range operatorPaths {
35 if path == p || strings.HasPrefix(path, p+ "/" ) {
36 return true
37 }
38 }
39 return false
40 }
41
42 // cleanPath drops a query string from a render path.
43 func cleanPath(path string ) string {
44 if i := strings.Index(path, "?" ); i >= 0 {
45 return path[:i]
46 }
47 return path
48 }
49
50 // Fallback is the built-in renderer. Render routes the operator views here
51 // directly, and themes delegate to it for every other path they do not draw
52 // themselves, so these views exist under any theme:
53 //
54 // "" the assembled page: the layout slot filled from the slots
55 // "slots" the slot index: name, size, revision, height of last write
56 // "slots/<slug>" one slot's raw markdown, fenced
57 // "edit" forms that write slots (they build the gnokey command)
58 // "edit/<slug>" the same, prefilled with one slot
59 // "system" who may write, which theme is live, pending, history
60 // "manifest" Manifest() as plain text, for scripts
61 func Fallback(path string ) string {
62 path = cleanPath(path)
63 switch {
64 case path == "" :
65 return renderPage()
66 case path == "slots" :
67 return renderIndex()
68 case strings.HasPrefix(path, "slots/" ):
69 return renderSlot(strings.TrimPrefix(path, "slots/" ))
70 case path == "edit" :
71 return renderEdit( "" )
72 case strings.HasPrefix(path, "edit/" ):
73 return renderEdit(strings.TrimPrefix(path, "edit/" ))
74 case path == "system" :
75 return renderSystem()
76 case path == "manifest" :
77 return Manifest()
78 }
79 return "# Not found\n\nNo such path: " + strconv.Quote(path) +
80 "\n\nTry [the slot index](" + Link( "slots" ) + ").\n"
81 }
82
83 // ---------------------------------------------------------------------------
84 // Placeholder filling
85
86 // Filler substitutes :name: placeholders in a layout. It is lazy: a callback
87 // runs only when its placeholder occurs in the layout, so an unused slot is
88 // never even read. Substitution is single-pass and non-recursive, so a
89 // placeholder inside a slot body is left alone and no slot can expand into
90 // another. Names never contain ':', so no placeholder is a prefix of another
91 // and the result does not depend on registration order. A placeholder that
92 // nothing claims survives into the output verbatim: a missing section should
93 // be visible, not silently blank.
94 type Filler struct {
95 names [] string
96 fns map [ string ] func () string
97 }
98
99 // NewFiller returns a Filler with every slot and every computed placeholder
100 // registered. A theme adds its own widgets with Add before calling Fill.
101 func NewFiller() *Filler {
102 f := &Filler{fns: map [ string ] func () string {}}
103 slots.Iterate( "" , "" , func (key string , value any ) bool {
104 s := value.(*Slot) // re-bound per iteration, so each closure sees its own
105 f.Add(key, func () string { return s.Body })
106 return false
107 })
108 f.Add( "realm" , func () string { return selfPath })
109 f.Add( "owner" , Authority)
110 f.Add( "chainid" , runtime.ChainID)
111 f.Add( "height" , func () string { return strconv.FormatInt(runtime.ChainHeight(), 10 ) })
112 f.Add( "rev" , func () string { return strconv.Itoa(rev) })
113 f.Add( "slots" , slotLinks)
114 f.Add( "theme" , func () string {
115 if p := LivePath(); p != "" {
116 return p
117 }
118 return "built-in"
119 })
120 f.Add( "updated" , updated)
121 return f
122 }
123
124 // updated is the :updated: placeholder: when the slots last changed.
125 func updated() string {
126 if lastHeight == 0 {
127 return "never"
128 }
129 return "block " + strconv.FormatInt(lastHeight, 10 ) + " (rev " + strconv.Itoa(rev) + ")"
130 }
131
132 // Add registers the callback for :name:. A later Add for the same name wins,
133 // which is how a theme widget overrides a slot of the same name.
134 func (f *Filler) Add(name string , fn func () string ) {
135 if _, dup := f.fns[name]; !dup {
136 f.names = append(f.names, name)
137 }
138 f.fns[name] = fn
139 }
140
141 // Fill returns layout with every present placeholder replaced.
142 func (f *Filler) Fill(layout string ) string {
143 pairs := [] string {}
144 for _, name := range f.names {
145 ph := ":" + name + ":"
146 if strings.Contains(layout, ph) {
147 pairs = append(pairs, ph, f.fns[name]())
148 }
149 }
150 if len(pairs) == 0 {
151 return layout
152 }
153 return strings.NewReplacer(pairs...).Replace(layout)
154 }
155
156 // ---------------------------------------------------------------------------
157 // Views
158
159 // defaultLayout renders before a layout slot exists and no theme is live,
160 // i.e. right after the very first deploy. It uses only computed placeholders,
161 // so it never shows an unresolved :slug: of its own.
162 func defaultLayout() string {
163 return "# " + selfPath + "\n\n" +
164 "No theme is live and no layout slot is set, so this is the built-in page. " +
165 "Content arrives with `Set`, one slot per call, and a theme deployed under " +
166 "`" + selfPath + "/theme/` goes live with `Accept`. " +
167 "See [system](" + Link( "system" ) + ") and [edit](" + Link( "edit" ) + ").\n\n" +
168 "## Slots\n\n:slots:\n\n---\n\n" +
169 "rev :rev: · block :height: · :chainid: · theme :theme: · " +
170 "[edit](" + Link( "edit" ) + ") · [system](" + Link( "system" ) + ")\n"
171 }
172
173 func renderPage() string {
174 layout := Layout()
175 if layout == "" {
176 layout = defaultLayout()
177 }
178 return NewFiller().Fill(layout)
179 }
180
181 // slotLinks is the :slots: placeholder: a bullet list of every slot, linking
182 // to its raw view.
183 func slotLinks() string {
184 if slots.Size() == 0 {
185 return "_no slots yet_"
186 }
187 var b strings.Builder
188 slots.Iterate( "" , "" , func (key string , _ any ) bool {
189 b.WriteString( "- [" + key + "](" + Link( "slots/" +key) + ")\n" )
190 return false
191 })
192 return strings.TrimSuffix(b.String(), "\n" )
193 }
194
195 func renderIndex() string {
196 var b strings.Builder
197 b.WriteString( "# Slots\n\n" )
198 b.WriteString( "rev " + strconv.Itoa(rev) + " · " + strconv.Itoa(slots.Size()) + " slot(s) · " +
199 "[edit](" + Link( "edit" ) + ") · [system](" + Link( "system" ) + ")\n\n" )
200 if slots.Size() == 0 {
201 b.WriteString( "_no slots yet_\n" )
202 return b.String()
203 }
204 b.WriteString( "| slot | bytes | rev | height | |\n" )
205 b.WriteString( "| --- | ---: | ---: | ---: | --- |\n" )
206 slots.Iterate( "" , "" , func (key string , value any ) bool {
207 s := value.(*Slot)
208 b.WriteString( "| [" + key + "](" + Link( "slots/" +key) + ") | " +
209 strconv.Itoa(len(s.Body)) + " | " +
210 strconv.Itoa(s.Rev) + " | " +
211 strconv.FormatInt(s.Height, 10 ) + " | " +
212 "[edit](" + Link( "edit/" +key) + ") |\n" )
213 return false
214 })
215 return b.String()
216 }
217
218 func renderSlot(slug string ) string {
219 s, ok := Lookup(slug)
220 if !ok {
221 return "# Not found\n\nNo slot named " + strconv.Quote(slug) +
222 ".\n\nTry [the slot index](" + Link( "slots" ) + ").\n"
223 }
224 f := fence(s.Body)
225 var b strings.Builder
226 b.WriteString( "# " + slug + "\n\n" )
227 b.WriteString(strconv.Itoa(len(s.Body)) + " bytes · rev " + strconv.Itoa(s.Rev) +
228 " · written at block " + strconv.FormatInt(s.Height, 10 ) +
229 " · [edit](" + Link( "edit/" +slug) + ")\n\n" )
230 b.WriteString(f + "\n" + s.Body)
231 if !strings.HasSuffix(s.Body, "\n" ) {
232 b.WriteString( "\n" )
233 }
234 b.WriteString(f + "\n" )
235 return b.String()
236 }
237
238 // fence returns a code fence longer than any backtick run in body, so the
239 // body can never close it early.
240 func fence(body string ) string {
241 longest, run := 0 , 0
242 for i := 0 ; i < len(body); i++ {
243 if body[i] != '`' {
244 run = 0
245 continue
246 }
247 run++
248 if run > longest {
249 longest = run
250 }
251 }
252 n := 3
253 if longest >= 3 {
254 n = longest + 1
255 }
256 return strings.Repeat( "`" , n)
257 }
258
259 // renderEdit is the in-browser editor: gnoweb forms that build the gnokey
260 // command for Set and Delete. Anyone can see them; only the authority's
261 // signature makes the resulting transaction succeed.
262 func renderEdit(slug string ) string {
263 var b strings.Builder
264 b.WriteString( "# Edit\n\n" )
265 b.WriteString( "Writes are restricted to `" + Authority() + "`. Each form assembles the " +
266 "`gnokey` command; sign it with that key. Slots and style knobs are the same thing: " +
267 "`style.accent` is a slot too. [Index](" + Link( "slots" ) + ") · [system](" + Link( "system" ) + ")\n\n" )
268
269 if slug != "" {
270 s, ok := Lookup(slug)
271 if !ok {
272 b.WriteString( "> [!WARNING]\n> No slot named " + strconv.Quote(slug) + " yet. Saving creates it.\n\n" )
273 }
274 b.WriteString( "## " + slug + "\n\n" )
275 b.WriteString( "<gno-form exec=\"Set\">\n" )
276 b.WriteString( " <gno-input name=\"slug\" type=\"text\" value=\"" + attr(slug) + "\" placeholder=\"slot name\" />\n" )
277 b.WriteString( " <gno-textarea name=\"body\" rows=\"10\" value=\"" + attr(s.Body) + "\" placeholder=\"markdown\" />\n" )
278 b.WriteString( "</gno-form>\n\n" )
279 if ok {
280 b.WriteString( "[Raw view](" + Link( "slots/" +slug) + ") · " +
281 "a body that itself contains the two characters `\\n` or `\\t` is unfolded here; " +
282 "use `make push` for those.\n\n" )
283 }
284 } else {
285 b.WriteString( "## New or replace\n\n" )
286 b.WriteString( "<gno-form exec=\"Set\">\n" )
287 b.WriteString( " <gno-input name=\"slug\" type=\"text\" placeholder=\"slot name: [a-z0-9._-]\" />\n" )
288 b.WriteString( " <gno-textarea name=\"body\" rows=\"10\" placeholder=\"markdown\" />\n" )
289 b.WriteString( "</gno-form>\n\n" )
290 }
291
292 b.WriteString( "## Delete\n\n" )
293 b.WriteString( "<gno-form exec=\"Delete\">\n" )
294 b.WriteString( " <gno-input name=\"slug\" type=\"text\" placeholder=\"slot name\" />\n" )
295 b.WriteString( "</gno-form>\n\n" )
296
297 b.WriteString( "## Slots\n\n" )
298 if slots.Size() == 0 {
299 b.WriteString( "_no slots yet_\n" )
300 return b.String()
301 }
302 slots.Iterate( "" , "" , func (key string , _ any ) bool {
303 b.WriteString( "- " + key + " · [view](" + Link( "slots/" +key) + ") · [edit](" + Link( "edit/" +key) + ")\n" )
304 return false
305 })
306 return b.String()
307 }
308
309 // attr escapes s for a double-quoted attribute of a gnoweb form tag, which
310 // must sit on one line: newlines and tabs become the \n and \t sequences the
311 // textarea unfolds.
312 func attr(s string ) string {
313 return strings.NewReplacer(
314 "&" , "&" ,
315 "\"" , """ ,
316 "<" , "<" ,
317 ">" , ">" ,
318 "\r" , "" ,
319 "\n" , "\\n" ,
320 "\t" , "\\t" ,
321 ).Replace(s)
322 }
323
324 // renderSystem shows the upgrade wiring and offers the Accept form.
325 func renderSystem() string {
326 var b strings.Builder
327 b.WriteString( "# System\n\n" )
328 b.WriteString( "| | |\n| --- | --- |\n" )
329 b.WriteString( "| realm | `" + selfPath + "` |\n" )
330 b.WriteString( "| authority | `" + Authority() + "` |\n" )
331 if p := LivePath(); p != "" {
332 b.WriteString( "| theme | [" + p + "](" + sourceLink(p) + ") |\n" )
333 } else {
334 b.WriteString( "| theme | built-in fallback (nothing accepted yet) |\n" )
335 }
336 b.WriteString( "| revision | " + strconv.Itoa(rev) + " |\n" )
337 b.WriteString( "| last write | " + updated() + " |\n" )
338 if Frozen() {
339 b.WriteString( "| upgrades | **frozen** |\n" )
340 } else {
341 b.WriteString( "| upgrades | open |\n" )
342 }
343 b.WriteString( "\n" )
344
345 pending := PendingPaths()
346 if len(pending) > 0 {
347 b.WriteString( "## Awaiting acceptance\n\n" )
348 b.WriteString( "A theme registered itself by being deployed. Accepting is a separate transaction, " +
349 "naming code you can [read first](" + sourceLink(pending[ 0 ]) + ").\n\n" )
350 for _, p := range pending {
351 b.WriteString( "<gno-form exec=\"Accept\">\n" )
352 b.WriteString( " <gno-input name=\"pkgPath\" type=\"text\" value=\"" + attr(p) + "\" placeholder=\"theme realm path\" />\n" )
353 b.WriteString( "</gno-form>\n\n" )
354 }
355 }
356
357 if past := HistoryPaths(); len(past) > 0 {
358 b.WriteString( "## Previously\n\n" )
359 for _, p := range past {
360 b.WriteString( "- [" + p + "](" + sourceLink(p) + ")\n" )
361 }
362 b.WriteString( "\n" )
363 }
364
365 if !Frozen() {
366 b.WriteString( "## Operations\n\n" )
367 b.WriteString( "- [Rollback](" + helpLink( "Rollback" ) + ") to the previous theme\n" )
368 b.WriteString( "- [Accept](" + helpLink( "Accept" ) + ") a theme by path\n" )
369 b.WriteString( "- [Withdraw](" + helpLink( "Withdraw" ) + ") a candidate\n" )
370 b.WriteString( "- [Forget](" + helpLink( "Forget" ) + ") the rollback history\n" )
371 b.WriteString( "- [Transfer authority](" + helpLink( "TransferAuthority" ) + ")\n" )
372 b.WriteString( "- [Freeze](" + helpLink( "Freeze" ) + "): no more theme upgrades, ever\n\n" )
373 }
374
375 b.WriteString( "## How an upgrade works\n\n" )
376 b.WriteString( "1. Copy `theme/v0` to `theme/v1`, change what you like, keep `Render`.\n" )
377 b.WriteString( "2. Deploy it at `" + selfPath + "/theme/v1`. Its init registers it here.\n" )
378 b.WriteString( "3. Accept that path. The slots never moved; only the code reading them changed.\n" )
379 b.WriteString( "4. If it misbehaves, Rollback. A theme that panics takes the page down until then, " +
380 "but this view and the other built-in ones never go through the theme, so the rollback is always reachable.\n" )
381 return b.String()
382 }
383
384 func helpLink(fn string ) string {
385 return Link( "" ) + "$help&func=" + fn
386 }
387
388 func sourceLink(pkgPath string ) string {
389 if i := strings.Index(pkgPath, "/" ); i >= 0 {
390 pkgPath = pkgPath[i:]
391 }
392 return pkgPath + "$source"
393 }
394
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.