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 rendered4// newest-first. Each post records its author (the calling address) and the5// block height at which it was published.6package blog78import (9 "strconv"10 "strings"1112 "chain"13 "chain/runtime"14 "chain/runtime/unsafe"1516 "gno.land/p/nt/avl/v0"17)1819// Post is a single blog entry.20type Post struct {21 ID int22 Title string23 Body string24 Author address25 Height int6426}2728// posts holds every published post keyed by its zero-padded id (for29// deterministic ordering) and nextID is the id assigned to the next post.30var (31 posts avl.Tree // key: zero-padded id string -> *Post32 nextID int33)3435// key returns a fixed-width, zero-padded key so avl.Tree iteration is ordered36// 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" + s41 }42 return s43}4445// 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 }5657 id := nextID58 nextID++5960 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)6869 chain.Emit(70 "PostPublished",71 "id", strconv.Itoa(id),72 "author", p.Author.String(),73 )74 return id75}7677// PostCount returns the number of published posts.78func PostCount() int {79 return posts.Size()80}8182// 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 line85 s := strings.ReplaceAll(body, "\n", " ")86 s = strings.TrimSpace(s)87 const max = 14088 if len(s) > max {89 return s[:max] + "…"90 }91 return s92}9394// Render lists all posts newest-first at the root, or a single post's full95// 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}112113func renderList() string {114 var b strings.Builder115 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")121122 // 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 false138 })139 return b.String()140}141142func renderPost(p *Post) string {143 var b strings.Builder144 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}156PostCount() int
Publish(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, title string, body string) int
Render(path string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.