← Back to Realms

Realm

gno.land/r/moul/x/daily/blog/v0

Overview

Realm Path
gno.land/r/moul/x/daily/blog/v0
Exported Functions
3
State Entries
3
Source Files
2
Total Package Entries
10

Exported Functions

3 exported functions

State

3 state entries

Source Code

FILES
blog.gno
go
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}
156

Raw Package Data

Raw JSON data