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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}