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

blog.gno

3.83 Kb · 155 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/nt/avl/v0"
 17)
 18
 19// Post is a single blog entry.
 20type Post struct {
 21	ID     int
 22	Title  string
 23	Body   string
 24	Author address
 25	Height int64
 26}
 27
 28// posts holds every published post keyed by its zero-padded id (for
 29// deterministic ordering) and nextID is the id assigned to the next post.
 30var (
 31	posts  avl.Tree // key: zero-padded id string -> *Post
 32	nextID int
 33)
 34
 35// key returns a fixed-width, zero-padded key so avl.Tree iteration is ordered
 36// by numeric id (and thus by publication order).
 37func key(id int) string {
 38	s := strconv.Itoa(id)
 39	for len(s) < 12 {
 40		s = "0" + s
 41	}
 42	return s
 43}
 44
 45// Publish creates a new post authored by the caller and returns its id.
 46// It is a state-mutating exported function, so it takes `cur realm`.
 47func Publish(cur realm, title string, body string) int {
 48	title = strings.TrimSpace(title)
 49	body = strings.TrimSpace(body)
 50	if title == "" {
 51		panic("blog: title must not be empty")
 52	}
 53	if body == "" {
 54		panic("blog: body must not be empty")
 55	}
 56
 57	id := nextID
 58	nextID++
 59
 60	p := &Post{
 61		ID:     id,
 62		Title:  title,
 63		Body:   body,
 64		Author: unsafe.PreviousRealm().Address(),
 65		Height: runtime.ChainHeight(),
 66	}
 67	posts.Set(key(id), p)
 68
 69	chain.Emit(
 70		"PostPublished",
 71		"id", strconv.Itoa(id),
 72		"author", p.Author.String(),
 73	)
 74	return id
 75}
 76
 77// PostCount returns the number of published posts.
 78func PostCount() int {
 79	return posts.Size()
 80}
 81
 82// snippet returns a short one-line preview of the body.
 83func snippet(body string) string {
 84	// collapse newlines so the preview stays on a single markdown line
 85	s := strings.ReplaceAll(body, "\n", " ")
 86	s = strings.TrimSpace(s)
 87	const max = 140
 88	if len(s) > max {
 89		return s[:max] + "…"
 90	}
 91	return s
 92}
 93
 94// Render lists all posts newest-first at the root, or a single post's full
 95// text at path "/<id>". Render is NOT a crossing function.
 96func Render(path string) string {
 97	path = strings.TrimSpace(path)
 98	if path == "" || path == "/" {
 99		return renderList()
100	}
101	idStr := strings.TrimPrefix(path, "/")
102	id, err := strconv.Atoi(idStr)
103	if err != nil {
104		return "# Not found\n\nInvalid post path: `" + path + "`\n\n[← back](/r/REPLACE_ADDR/blog)\n"
105	}
106	v := posts.Get(key(id))
107	if v == nil {
108		return "# Not found\n\nNo post with id " + idStr + ".\n\n[← back](/r/REPLACE_ADDR/blog)\n"
109	}
110	return renderPost(v.(*Post))
111}
112
113func renderList() string {
114	var b strings.Builder
115	b.WriteString("# 📝 Blog\n\n")
116	if posts.Size() == 0 {
117		b.WriteString("_No posts yet. Be the first to `Publish`._\n")
118		return b.String()
119	}
120	b.WriteString(strconv.Itoa(posts.Size()) + " post(s), newest first.\n\n")
121
122	// avl iterates ascending; iterate in reverse for newest-first.
123	posts.ReverseIterate("", "", func(_ string, v interface{}) bool {
124		p := v.(*Post)
125		b.WriteString("## [")
126		b.WriteString(p.Title)
127		b.WriteString("](/r/REPLACE_ADDR/blog:/")
128		b.WriteString(strconv.Itoa(p.ID))
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(snippet(p.Body))
136		b.WriteString("\n\n---\n\n")
137		return false
138	})
139	return b.String()
140}
141
142func renderPost(p *Post) string {
143	var b strings.Builder
144	b.WriteString("# ")
145	b.WriteString(p.Title)
146	b.WriteString("\n\n")
147	b.WriteString("by `")
148	b.WriteString(p.Author.String())
149	b.WriteString("` · height ")
150	b.WriteString(strconv.FormatInt(p.Height, 10))
151	b.WriteString("\n\n")
152	b.WriteString(p.Body)
153	b.WriteString("\n\n---\n\n[← back to all posts](/r/REPLACE_ADDR/blog)\n")
154	return b.String()
155}