PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/gnoland/blog/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
gnoland / blog
Files
5 (gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/gnoland/blog/v0
gno
0.9

Files (5)

  • gnomod.tomltoml
  • blog.gnogno
  • errors.gnogno
  • util.gnogno
  • blog_test.gnogno
blog.gnogno
1package blog23import (4	"strconv"5	"strings"6	"time"78	"gno.land/p/nt/avl/v0"9	"gno.land/p/nt/mux/v0"10	"gno.land/p/nt/ufmt/v0"11)1213type Blog struct {14	Title             string15	Prefix            string   // i.e. r/gnoland/blog:16	Posts             avl.Tree // slug -> *Post17	PostsPublished    avl.Tree // published-date -> *Post18	PostsAlphabetical avl.Tree // title -> *Post19	NoBreadcrumb      bool20}2122func (b Blog) RenderLastPostsWidget(limit int) string {23	if b.PostsPublished.Size() == 0 {24		return "No posts."25	}2627	output := ""28	i := 029	b.PostsPublished.ReverseIterate("", "", func(key string, value any) bool {30		p := value.(*Post)31		output += ufmt.Sprintf("- [%s](%s)\n", p.Title, p.URL())32		i++33		return i >= limit34	})35	return output36}3738func (b Blog) RenderHome(res *mux.ResponseWriter, _ *mux.Request) {39	if !b.NoBreadcrumb {40		res.Write(breadcrumb([]string{b.Title}))41	}4243	if b.Posts.Size() == 0 {44		res.Write("No posts.")45		return46	}4748	const maxCol = 349	var rowItems []string5051	b.PostsPublished.ReverseIterate("", "", func(key string, value any) bool {52		post := value.(*Post)53		rowItems = append(rowItems, post.RenderListItem())5455		if len(rowItems) == maxCol {56			res.Write("<gno-columns>" + strings.Join(rowItems, "<gno-columns-sep>") + "</gno-columns>\n")57			rowItems = []string{}58		}59		return false60	})6162	// Pad and flush any remaining items63	if len(rowItems) > 0 {64		for len(rowItems) < maxCol {65			rowItems = append(rowItems, "")66		}67		res.Write("<gno-columns>" + strings.Join(rowItems, "\n<gno-columns-sep>\n") + "</gno-columns>\n")68	}69}7071func (b Blog) RenderPost(res *mux.ResponseWriter, req *mux.Request) {72	slug := req.GetVar("slug")7374	post := b.Posts.Get(slug)75	if post == nil {76		res.Write("404")77		return78	}79	p := post.(*Post)8081	res.Write("<main class='gno-tmpl-page'>" + "\n\n")8283	res.Write("# " + p.Title + "\n\n")84	res.Write(p.Body + "\n\n")85	res.Write("---\n\n")8687	res.Write(p.RenderTagList() + "\n\n")88	res.Write(p.RenderAuthorList() + "\n\n")89	res.Write(p.RenderPublishData() + "\n\n")9091	res.Write("---\n")92	res.Write("<details><summary>Comment section</summary>\n\n")9394	// comments95	p.Comments.ReverseIterate("", "", func(key string, value any) bool {96		comment := value.(*Comment)97		res.Write(comment.RenderListItem())98		return false99	})100101	res.Write("</details>\n")102	res.Write("</main>")103}104105func (b Blog) RenderTag(res *mux.ResponseWriter, req *mux.Request) {106	slug := req.GetVar("slug")107108	if slug == "" {109		res.Write("404")110		return111	}112113	if !b.NoBreadcrumb {114		breadStr := breadcrumb([]string{115			ufmt.Sprintf("[%s](%s)", b.Title, b.Prefix),116			"t",117			slug,118		})119		res.Write(breadStr)120	}121122	nb := 0123	b.Posts.Iterate("", "", func(key string, value any) bool {124		post := value.(*Post)125		if !post.HasTag(slug) {126			return false127		}128		res.Write(post.RenderListItem())129		nb++130		return false131	})132	if nb == 0 {133		res.Write("No posts.")134	}135}136137func (b Blog) Render(path string) string {138	router := mux.NewRouter()139	router.HandleFunc("", b.RenderHome)140	router.HandleFunc("p/{slug}", b.RenderPost)141	router.HandleFunc("t/{slug}", b.RenderTag)142	return router.Render(path)143}144145func (b *Blog) NewPost(publisher address, slug, title, body, pubDate string, authors, tags []string) error {146	if b.Posts.Has(slug) {147		return ErrPostSlugExists148	}149150	var parsedTime time.Time151	var err error152	if pubDate != "" {153		parsedTime, err = time.Parse(time.RFC3339, pubDate)154		if err != nil {155			return err156		}157	} else {158		// If no publication date was passed in by caller, take current block time159		parsedTime = time.Now()160	}161162	post := &Post{163		Publisher: publisher,164		Authors:   authors,165		Slug:      slug,166		Title:     title,167		Body:      body,168		Tags:      tags,169		CreatedAt: parsedTime,170	}171172	return b.prepareAndSetPost(post, false)173}174175func (b *Blog) prepareAndSetPost(post *Post, edit bool) error {176	post.Title = strings.TrimSpace(post.Title)177	post.Body = strings.TrimSpace(post.Body)178179	if post.Title == "" {180		return ErrPostTitleMissing181	}182	if post.Body == "" {183		return ErrPostBodyMissing184	}185	if post.Slug == "" {186		return ErrPostSlugMissing187	}188189	post.Blog = b190	post.UpdatedAt = time.Now()191192	trimmedTitleKey := getTitleKey(post.Title)193	pubDateKey := getPublishedKey(post.CreatedAt)194195	if !edit {196		// Cannot have two posts with same title key197		if b.PostsAlphabetical.Has(trimmedTitleKey) {198			return ErrPostTitleExists199		}200		// Cannot have two posts with *exact* same timestamp201		if b.PostsPublished.Has(pubDateKey) {202			return ErrPostPubDateExists203		}204	}205206	// Store post under keys207	b.PostsAlphabetical.Set(trimmedTitleKey, post)208	b.PostsPublished.Set(pubDateKey, post)209	b.Posts.Set(post.Slug, post)210211	return nil212}213214func (b *Blog) RemovePost(slug string) {215	p := b.Posts.Get(slug)216	if p == nil {217		panic("post with specified slug doesn't exist")218	}219220	post := p.(*Post)221222	titleKey := getTitleKey(post.Title)223	publishedKey := getPublishedKey(post.CreatedAt)224225	_, _ = b.Posts.Remove(slug)226	_, _ = b.PostsAlphabetical.Remove(titleKey)227	_, _ = b.PostsPublished.Remove(publishedKey)228}229230func (b *Blog) GetPost(slug string) *Post {231	post := b.Posts.Get(slug)232	if post == nil {233		return nil234	}235	return post.(*Post)236}237238type Post struct {239	Blog         *Blog240	Slug         string // FIXME: save space?241	Title        string242	Body         string243	CreatedAt    time.Time244	UpdatedAt    time.Time245	Comments     avl.Tree246	Authors      []string247	Publisher    address248	Tags         []string249	CommentIndex int250}251252func (p *Post) Update(title, body, publicationDate string, authors, tags []string) error {253	p.Title = title254	p.Body = body255	p.Tags = tags256	p.Authors = authors257258	parsedTime, err := time.Parse(time.RFC3339, publicationDate)259	if err != nil {260		return err261	}262263	p.CreatedAt = parsedTime264	return p.Blog.prepareAndSetPost(p, true)265}266267func (p *Post) AddComment(author address, comment string) error {268	if p == nil {269		return ErrNoSuchPost270	}271	p.CommentIndex++272	commentKey := strconv.Itoa(p.CommentIndex)273	comment = strings.TrimSpace(comment)274	p.Comments.Set(commentKey, &Comment{275		Post:      p,276		CreatedAt: time.Now(),277		Author:    author,278		Comment:   comment,279	})280281	return nil282}283284func (p *Post) DeleteComment(index int) error {285	if p == nil {286		return ErrNoSuchPost287	}288	commentKey := strconv.Itoa(index)289	p.Comments.Remove(commentKey)290	return nil291}292293func (p *Post) HasTag(tag string) bool {294	if p == nil {295		return false296	}297	for _, t := range p.Tags {298		if t == tag {299			return true300		}301	}302	return false303}304305func (p *Post) RenderListItem() string {306	if p == nil {307		return "error: no such post\n"308	}309	output := ufmt.Sprintf("\n### [%s](%s)\n", p.Title, p.URL())310	// output += ufmt.Sprintf("**[Learn More](%s)**\n\n", p.URL())311312	output += p.CreatedAt.Format("02 Jan 2006")313	// output += p.Summary() + "\n\n"314	// output += p.RenderTagList() + "\n\n"315	output += "\n"316	return output317}318319// Render post tags320func (p *Post) RenderTagList() string {321	if p == nil {322		return "error: no such post\n"323	}324	if len(p.Tags) == 0 {325		return ""326	}327328	output := "Tags: "329	for idx, tag := range p.Tags {330		if idx > 0 {331			output += " "332		}333		tagURL := p.Blog.Prefix + "t/" + tag334		output += ufmt.Sprintf("[#%s](%s)", tag, tagURL)335336	}337	return output338}339340// Render authors if there are any341func (p *Post) RenderAuthorList() string {342	out := "Written"343	if len(p.Authors) != 0 {344		out += " by "345346		for idx, author := range p.Authors {347			out += author348			if idx < len(p.Authors)-1 {349				out += ", "350			}351		}352	}353	out += " on " + p.CreatedAt.Format("02 Jan 2006")354355	return out356}357358func (p *Post) RenderPublishData() string {359	out := "Published "360	if p.Publisher != "" {361		out += "by " + p.Publisher.String() + " "362	}363	out += "to " + p.Blog.Title364365	return out366}367368func (p *Post) URL() string {369	if p == nil {370		return p.Blog.Prefix + "404"371	}372	return p.Blog.Prefix + "p/" + p.Slug373}374375func (p *Post) Summary() string {376	if p == nil {377		return "error: no such post\n"378	}379380	// FIXME: better summary.381	lines := strings.Split(p.Body, "\n")382	if len(lines) <= 3 {383		return p.Body384	}385	return strings.Join(lines[0:3], "\n") + "..."386}387388type Comment struct {389	Post      *Post390	CreatedAt time.Time391	Author    address392	Comment   string393}394395func (c Comment) RenderListItem() string {396	output := "<h5>"397	output += c.Comment + "\n\n"398	output += "</h5>"399400	output += "<h6>"401	output += ufmt.Sprintf("by %s on %s", c.Author, c.CreatedAt.Format(time.RFC822))402	output += "</h6>\n\n"403404	output += "---\n\n"405406	return output407}408

Functions

not supported for pure packages by the node (vm/qfuncs)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.