1// Package microblog is a public microblog wall for gno.land.2//3// Anyone can Post a short message (<= maxMsgLen chars). Every post records4// the caller address, the message, and the block height at which it was made.5// Render displays the wall newest-first, 20 posts per page, with pagination6// driven by the Render path (e.g. "?page=2" or "/2").7package microblog89import (10 "chain"11 "chain/runtime"12 "chain/runtime/unsafe"13 "strconv"14 "strings"15)1617// maxMsgLen is the maximum length (in bytes) of a single post.18const maxMsgLen = 2801920// pageSize is how many posts a single Render page shows.21const pageSize = 202223// post is a single microblog entry.24type post struct {25 Author string // bech32 address of the poster26 Msg string // the message body27 Height int64 // block height at which it was posted28}2930// posts is the append-only wall, in chronological (oldest-first) order.31// Render reverses this view to show newest-first.32var posts []post3334// Post publishes msg to the wall. It is a crossing function: callers invoke it35// with Post(cross(cur), "hello"). It panics if msg is empty or longer than36// maxMsgLen bytes.37func Post(cur realm, msg string) {38 if !cur.IsCurrent() {39 panic("spoofed realm: cur is not the live crossing frame")40 }41 if len(msg) == 0 {42 panic("microblog: empty message")43 }44 if len(msg) > maxMsgLen {45 panic("microblog: message too long (max " + strconv.Itoa(maxMsgLen) + " bytes)")46 }4748 author := unsafe.PreviousRealm().Address()4950 posts = append(posts, post{51 Author: author.String(),52 Msg: msg,53 Height: runtime.ChainHeight(),54 })5556 chain.Emit("Post", "author", author.String(), "len", strconv.Itoa(len(msg)))57}5859// Count returns the total number of posts on the wall.60func Count() int {61 return len(posts)62}6364// Render renders the wall as Markdown, newest-first, pageSize posts per page.65// The page is parsed from path: "?page=N", "?p=N", "/N", or a bare "N" all66// select page N (1-indexed). Anything else defaults to page 1.67func Render(path string) string {68 total := len(posts)69 page := parsePage(path)7071 var b strings.Builder72 b.WriteString("# Microblog Wall\n\n")7374 if total == 0 {75 b.WriteString("_No posts yet. Be the first to post._\n")76 return b.String()77 }7879 pages := (total + pageSize - 1) / pageSize80 if page < 1 {81 page = 182 }83 if page > pages {84 page = pages85 }8687 b.WriteString(strconv.Itoa(total))88 b.WriteString(" posts total · page ")89 b.WriteString(strconv.Itoa(page))90 b.WriteString(" of ")91 b.WriteString(strconv.Itoa(pages))92 b.WriteString("\n\n")9394 // Newest-first: post index total-1 is newest. For page p (1-indexed),95 // skip (p-1)*pageSize newest posts, then take up to pageSize.96 start := total - 1 - (page-1)*pageSize97 end := start - pageSize + 198 if end < 0 {99 end = 0100 }101102 for i := start; i >= end; i-- {103 p := posts[i]104 b.WriteString("- **")105 b.WriteString(shortAddr(p.Author))106 b.WriteString("** · height ")107 b.WriteString(strconv.FormatInt(p.Height, 10))108 b.WriteString("\n\n ")109 b.WriteString(escapeInline(p.Msg))110 b.WriteString("\n")111 }112113 b.WriteString("\n")114 b.WriteString(renderNav(page, pages))115 return b.String()116}117118// renderNav builds a simple prev/next navigation footer as Markdown links.119func renderNav(page, pages int) string {120 var b strings.Builder121 if page > 1 {122 b.WriteString("[← newer](?page=")123 b.WriteString(strconv.Itoa(page - 1))124 b.WriteString(")")125 }126 if page > 1 && page < pages {127 b.WriteString(" · ")128 }129 if page < pages {130 b.WriteString("[older →](?page=")131 b.WriteString(strconv.Itoa(page + 1))132 b.WriteString(")")133 }134 if b.Len() == 0 {135 return ""136 }137 return b.String() + "\n"138}139140// parsePage extracts the 1-indexed page number from a Render path. It accepts141// "?page=N", "?p=N", "/N", and a bare "N". Unrecognized input yields page 1.142func parsePage(path string) int {143 path = strings.TrimSpace(path)144 if path == "" {145 return 1146 }147148 // Query form: "?page=2&foo=bar" or "?p=2".149 if idx := strings.IndexByte(path, '?'); idx >= 0 {150 query := path[idx+1:]151 for _, part := range strings.Split(query, "&") {152 kv := strings.SplitN(part, "=", 2)153 if len(kv) != 2 {154 continue155 }156 if kv[0] == "page" || kv[0] == "p" {157 if n, err := strconv.Atoi(strings.TrimSpace(kv[1])); err == nil {158 return n159 }160 }161 }162 return 1163 }164165 // Path form: "/2" or "2".166 seg := strings.TrimPrefix(path, "/")167 if n, err := strconv.Atoi(seg); err == nil {168 return n169 }170 return 1171}172173// shortAddr abbreviates a bech32 address for display: g1abcd…wxyz.174func shortAddr(addr string) string {175 if len(addr) <= 12 {176 return addr177 }178 return addr[:6] + "…" + addr[len(addr)-4:]179}180181// escapeInline neutralizes characters that would break the Markdown list item182// layout (newlines collapse to spaces; leading markers are defused).183func escapeInline(s string) string {184 s = strings.ReplaceAll(s, "\r\n", " ")185 s = strings.ReplaceAll(s, "\n", " ")186 s = strings.ReplaceAll(s, "\r", " ")187 return s188}189Count() int
Post(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}, msg string)
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.