blocks.gno
3.24 Kb · 85 lines
1package config
2
3import "strings"
4
5// The notice blocks: two strings a realm drops at the top and the bottom of its
6// Render, empty by default, so moul can put a warning, a changelog line or a
7// bit of news on every realm at once or on one of them.
8//
9// func Render(path string) string {
10// return config.TopBlock() + body + config.BottomBlock()
11// }
12//
13// Both are set with the ordinary Set, which is what keeps this realm's surface
14// from growing a function per idea:
15//
16// Set block.top "> Chain migration on Tuesday." # every realm
17// Set block.top@r/moul/gns "> v2 shipped, see the changelog" # this one only
18const (
19 // KeyBlockTop is the notice rendered above a realm's content.
20 KeyBlockTop = "block.top"
21 // KeyBlockBottom is the notice rendered below it.
22 KeyBlockBottom = "block.bottom"
23)
24
25// TopBlock returns the notice for the realm calling in, ready to concatenate
26// in front of its content. Empty when there is nothing to say, which is the
27// default and the usual case.
28//
29// Three things can appear, in this order, separated by blank lines:
30//
31// 1. the pause banner, when this realm or every realm is paused
32// 2. the global block.top
33// 3. this realm's own block.top
34//
35// The pause banner comes first because it is the one a reader has to see, and
36// it is included here rather than left to the realm so that guarding writes
37// with AssertWritable is enough to also explain the refusal on the page.
38//
39// Global and scoped are BOTH shown rather than one overriding the other: a
40// chain-wide warning and a per-realm changelog are different messages, and
41// dropping either because the other exists is the surprising behaviour.
42func TopBlock() string { return TopBlockFor(caller()) }
43
44// TopBlockFor is TopBlock for a named realm. Use it from a crossing function,
45// where the caller cannot be read off the stack, or to render another realm's
46// notice.
47func TopBlockFor(pkgPath string) string {
48 global, scoped := scopedPair(KeyBlockTop, pkgPath)
49 return joinBlocks(PauseFor(pkgPath).Notice(), global, scoped)
50}
51
52// BottomBlock returns the notice for the realm calling in, ready to
53// concatenate after its content.
54//
55// No pause banner here: one is enough, and the top is where it is read.
56func BottomBlock() string { return BottomBlockFor(caller()) }
57
58// BottomBlockFor is BottomBlock for a named realm.
59func BottomBlockFor(pkgPath string) string {
60 global, scoped := scopedPair(KeyBlockBottom, pkgPath)
61 return joinBlocks(global, scoped)
62}
63
64// joinBlocks assembles the non-empty parts into one markdown fragment.
65//
66// Each part is separated by a blank line, because two markdown blocks with
67// only a newline between them are one paragraph, and a blockquote followed
68// directly by text swallows the text into the quote.
69//
70// The result ends with a blank line when there is anything at all, so a realm
71// can write config.TopBlock() + body without a separator of its own and get
72// the same output whether or not a notice is set. Nothing to say means the
73// empty string, and the page is byte-for-byte what it was before.
74func joinBlocks(parts ...string) string {
75 var kept []string
76 for _, p := range parts {
77 if p = strings.TrimSpace(p); p != "" {
78 kept = append(kept, p)
79 }
80 }
81 if len(kept) == 0 {
82 return ""
83 }
84 return strings.Join(kept, "\n\n") + "\n\n"
85}