blog.gno
3.97 Kb · 141 lines
1// Package blog is a multi-author on-chain blog realm for gno.land.
2//
3// Any caller can Publish a post; posts are stored persistently and rendered
4// newest-first. Each post records its author (the calling address) and the
5// block height at which it was published.
6package blog
7
8import (
9 "strconv"
10 "strings"
11
12 "chain"
13 "chain/runtime"
14
15 "gno.land/p/nym-moultest123/kit/store/v0"
16 "gno.land/p/nym-moultest123/kit/ui/v0"
17 "gno.land/p/nym-moultest123/md/v0"
18)
19
20// selfPath is this realm's own page. A root-relative link resolves against
21// the domain, not the realm, so every link back into it names it in full.
22const selfPath = "/r/nym-moultest123/x/daily/blog/v1"
23
24// Post is a single blog entry. It carries no id field: the id belongs to the
25// store, which hands it back on lookup and iteration.
26type Post struct {
27 Title string
28 Body string
29 Author address
30 Height int64
31}
32
33// posts holds every published post. The store keys by seqid, so iteration is
34// in publication order for any number of posts; v0 kept its own nextID plus a
35// key() that zero-padded to width 12 and stopped ordering past it.
36var posts = store.Named("post")
37
38// Publish creates a new post authored by the caller and returns its id.
39// It is a state-mutating exported function, so it takes `cur realm`.
40func Publish(cur realm, title string, body string) int {
41 if !cur.IsCurrent() {
42 panic("blog: spoofed realm")
43 }
44 title = strings.TrimSpace(title)
45 body = strings.TrimSpace(body)
46 if title == "" {
47 panic("blog: title must not be empty")
48 }
49 if body == "" {
50 panic("blog: body must not be empty")
51 }
52
53 author := cur.Previous().Address()
54 id := posts.Add(&Post{
55 Title: title,
56 Body: body,
57 Author: author,
58 Height: runtime.ChainHeight(),
59 })
60
61 chain.Emit(
62 "PostPublished",
63 "id", id.String(),
64 "author", author.String(),
65 )
66 return int(id)
67}
68
69// PostCount returns the number of published posts.
70func PostCount() int {
71 return posts.Len()
72}
73
74// Render lists all posts newest-first at the root, or a single post's full
75// text at path "/<id>". Render is NOT a crossing function.
76func Render(path string) string {
77 path = strings.TrimSpace(path)
78 if path == "" || path == "/" {
79 return renderList()
80 }
81 idStr := strings.TrimPrefix(path, "/")
82 id, ok := store.ParseID(idStr)
83 if !ok {
84 return "# Not found\n\nInvalid post path: " + md.InlineCode(path) + "\n\n[← back](" + selfPath + ")\n"
85 }
86 v, ok := posts.Get(id)
87 if !ok {
88 return "# Not found\n\nNo post with id " + idStr + ".\n\n[← back](" + selfPath + ")\n"
89 }
90 return renderPost(v.(*Post))
91}
92
93func renderList() string {
94 var b strings.Builder
95 b.WriteString("# 📝 Blog\n\n")
96 if posts.Len() == 0 {
97 b.WriteString("_No posts yet. Be the first to `Publish`._\n")
98 return b.String()
99 }
100 b.WriteString(strconv.Itoa(posts.Len()) + " post(s), newest first.\n\n")
101
102 // Ids ascend with publication, so reverse iteration is newest-first.
103 posts.EachReverse(func(id store.ID, v any) {
104 p := v.(*Post)
105 b.WriteString("## [")
106 b.WriteString(ui.Inline(p.Title))
107 b.WriteString("](" + selfPath + ":/")
108 b.WriteString(id.String())
109 b.WriteString(")\n\n")
110 b.WriteString("by `")
111 b.WriteString(p.Author.String())
112 b.WriteString("` · height ")
113 b.WriteString(strconv.FormatInt(p.Height, 10))
114 b.WriteString("\n\n")
115 b.WriteString(ui.Excerpt(strings.TrimSpace(p.Body), 140))
116 b.WriteString("\n\n---\n\n")
117 })
118 return b.String()
119}
120
121func renderPost(p *Post) string {
122 var b strings.Builder
123 b.WriteString("# ")
124 b.WriteString(ui.Inline(p.Title))
125 b.WriteString("\n\n")
126 b.WriteString("by `")
127 b.WriteString(p.Author.String())
128 b.WriteString("` · height ")
129 b.WriteString(strconv.FormatInt(p.Height, 10))
130 b.WriteString("\n\n")
131 // Anyone may publish, so the body is the caller's text, not this realm's
132 // markdown: each line is escaped, and a blank line still breaks a paragraph.
133 for i, line := range strings.Split(p.Body, "\n") {
134 if i > 0 {
135 b.WriteString(" \n")
136 }
137 b.WriteString(ui.Inline(line))
138 }
139 b.WriteString("\n\n---\n\n[← back to all posts](" + selfPath + ")\n")
140 return b.String()
141}