Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

blog.gno

3.65 Kb · 138 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	"chain/runtime/unsafe"
 15
 16	"gno.land/p/moul/kit/store/v0"
 17)
 18
 19// Post is a single blog entry. It carries no id field: the id belongs to the
 20// store, which hands it back on lookup and iteration.
 21type Post struct {
 22	Title  string
 23	Body   string
 24	Author address
 25	Height int64
 26}
 27
 28// posts holds every published post. The store keys by seqid, so iteration is
 29// in publication order for any number of posts; v0 kept its own nextID plus a
 30// key() that zero-padded to width 12 and stopped ordering past it.
 31var posts = store.Named("post")
 32
 33// Publish creates a new post authored by the caller and returns its id.
 34// It is a state-mutating exported function, so it takes `cur realm`.
 35func Publish(cur realm, title string, body string) int {
 36	title = strings.TrimSpace(title)
 37	body = strings.TrimSpace(body)
 38	if title == "" {
 39		panic("blog: title must not be empty")
 40	}
 41	if body == "" {
 42		panic("blog: body must not be empty")
 43	}
 44
 45	author := unsafe.PreviousRealm().Address()
 46	id := posts.Add(&Post{
 47		Title:  title,
 48		Body:   body,
 49		Author: author,
 50		Height: runtime.ChainHeight(),
 51	})
 52
 53	chain.Emit(
 54		"PostPublished",
 55		"id", id.String(),
 56		"author", author.String(),
 57	)
 58	return int(id)
 59}
 60
 61// PostCount returns the number of published posts.
 62func PostCount() int {
 63	return posts.Len()
 64}
 65
 66// snippet returns a short one-line preview of the body.
 67func snippet(body string) string {
 68	// collapse newlines so the preview stays on a single markdown line
 69	s := strings.ReplaceAll(body, "\n", " ")
 70	s = strings.TrimSpace(s)
 71	const max = 140
 72	if len(s) > max {
 73		return s[:max] + "…"
 74	}
 75	return s
 76}
 77
 78// Render lists all posts newest-first at the root, or a single post's full
 79// text at path "/<id>". Render is NOT a crossing function.
 80func Render(path string) string {
 81	path = strings.TrimSpace(path)
 82	if path == "" || path == "/" {
 83		return renderList()
 84	}
 85	idStr := strings.TrimPrefix(path, "/")
 86	id, ok := store.ParseID(idStr)
 87	if !ok {
 88		return "# Not found\n\nInvalid post path: `" + path + "`\n\n[← back](/r/REPLACE_ADDR/blog)\n"
 89	}
 90	v, ok := posts.Get(id)
 91	if !ok {
 92		return "# Not found\n\nNo post with id " + idStr + ".\n\n[← back](/r/REPLACE_ADDR/blog)\n"
 93	}
 94	return renderPost(v.(*Post))
 95}
 96
 97func renderList() string {
 98	var b strings.Builder
 99	b.WriteString("# 📝 Blog\n\n")
100	if posts.Len() == 0 {
101		b.WriteString("_No posts yet. Be the first to `Publish`._\n")
102		return b.String()
103	}
104	b.WriteString(strconv.Itoa(posts.Len()) + " post(s), newest first.\n\n")
105
106	// Ids ascend with publication, so reverse iteration is newest-first.
107	posts.EachReverse(func(id store.ID, v any) {
108		p := v.(*Post)
109		b.WriteString("## [")
110		b.WriteString(p.Title)
111		b.WriteString("](/r/REPLACE_ADDR/blog:/")
112		b.WriteString(id.String())
113		b.WriteString(")\n\n")
114		b.WriteString("by `")
115		b.WriteString(p.Author.String())
116		b.WriteString("` · height ")
117		b.WriteString(strconv.FormatInt(p.Height, 10))
118		b.WriteString("\n\n")
119		b.WriteString(snippet(p.Body))
120		b.WriteString("\n\n---\n\n")
121	})
122	return b.String()
123}
124
125func renderPost(p *Post) string {
126	var b strings.Builder
127	b.WriteString("# ")
128	b.WriteString(p.Title)
129	b.WriteString("\n\n")
130	b.WriteString("by `")
131	b.WriteString(p.Author.String())
132	b.WriteString("` · height ")
133	b.WriteString(strconv.FormatInt(p.Height, 10))
134	b.WriteString("\n\n")
135	b.WriteString(p.Body)
136	b.WriteString("\n\n---\n\n[← back to all posts](/r/REPLACE_ADDR/blog)\n")
137	return b.String()
138}