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

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