Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

render.gno

4.09 Kb · 165 lines
  1package blog
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"gno.land/p/moul/kit/ui/v0"
  8)
  9
 10// linkPrefix is what a permalink starts with. gnoweb addresses a realm's
 11// render path with ':' after the package path, so this is realmPath with the
 12// leading "gno.land" dropped and the separator appended.
 13const linkPrefix = "/r/moul/blog:"
 14
 15// defaultIntro is the header shown before SetIntro has ever been called, i.e.
 16// right after the first deploy or after a redeploy wiped the state.
 17const defaultIntro = "# moul's blog\n\nNotes on gno.land, from the person building on it.\n"
 18
 19// Render serves three views:
 20//
 21//	""         the index, newest first
 22//	"<slug>"   one post
 23//	"t/<tag>"  every post carrying a tag
 24func Render(path string) string {
 25	path = strings.TrimSpace(path)
 26	switch {
 27	case path == "":
 28		return renderIndex()
 29	case strings.HasPrefix(path, "t/"):
 30		return renderTag(strings.TrimPrefix(path, "t/"))
 31	default:
 32		return renderPost(path)
 33	}
 34}
 35
 36func header() string {
 37	if intro == "" {
 38		return defaultIntro
 39	}
 40	return strings.TrimRight(intro, "\n") + "\n"
 41}
 42
 43func renderIndex() string {
 44	var b strings.Builder
 45	b.WriteString(header())
 46
 47	if posts.Size() == 0 {
 48		b.WriteString("\n")
 49		b.WriteString(ui.Empty("Nothing published yet."))
 50		return b.String()
 51	}
 52
 53	// order is keyed by "<date>/<slug>", so walking it backwards is
 54	// newest-first without a sort the language does not have.
 55	order.ReverseIterate("", "", func(_ string, value any) bool {
 56		b.WriteString("\n")
 57		b.WriteString(entry(value.(*post)))
 58		return false
 59	})
 60	return b.String()
 61}
 62
 63func renderTag(tag string) string {
 64	tag = strings.ToLower(strings.TrimSpace(tag))
 65
 66	var b strings.Builder
 67	b.WriteString("# Tagged ")
 68	b.WriteString(ui.Inline(tag))
 69	b.WriteString("\n")
 70
 71	found := 0
 72	order.ReverseIterate("", "", func(_ string, value any) bool {
 73		p := value.(*post)
 74		if !hasTag(p, tag) {
 75			return false
 76		}
 77		found++
 78		b.WriteString("\n")
 79		b.WriteString(entry(p))
 80		return false
 81	})
 82	if found == 0 {
 83		b.WriteString("\n")
 84		b.WriteString(ui.Empty("No post carries this tag."))
 85	}
 86	b.WriteString("\n[← all posts](" + linkPrefix + ")\n")
 87	return b.String()
 88}
 89
 90func renderPost(slug string) string {
 91	v := posts.Get(slug)
 92	if v == nil {
 93		return "# Not found\n\nNo post at " + strconv.Quote(ui.Inline(slug)) +
 94			".\n\n[← all posts](" + linkPrefix + ")\n"
 95	}
 96	p := v.(*post)
 97
 98	var b strings.Builder
 99	b.WriteString("# ")
100	b.WriteString(ui.Inline(p.title))
101	b.WriteString("\n\n")
102	b.WriteString(byline(p))
103	b.WriteString("\n\n")
104	// The body is markdown written by `author`, who is also the only address
105	// that can call Set. It is the post, so it renders verbatim: escaping it
106	// would turn every heading and link in it into literal text.
107	b.WriteString(strings.TrimRight(p.body, "\n"))
108	b.WriteString("\n\n---\n\n[← all posts](" + linkPrefix + ")\n")
109	return b.String()
110}
111
112// entry is one row of a listing: linked title, byline, excerpt.
113func entry(p *post) string {
114	var b strings.Builder
115	b.WriteString("## [")
116	b.WriteString(ui.Inline(p.title))
117	b.WriteString("](")
118	b.WriteString(linkPrefix)
119	b.WriteString(p.slug)
120	b.WriteString(")\n\n")
121	b.WriteString(byline(p))
122	b.WriteString("\n\n")
123	b.WriteString(ui.Excerpt(firstLine(p.body), excerptLen))
124	b.WriteString("\n")
125	return b.String()
126}
127
128// byline is the date plus the post's tags as links, on one line.
129func byline(p *post) string {
130	var b strings.Builder
131	b.WriteString(p.date)
132	for _, t := range splitTags(p.tags) {
133		b.WriteString(" · [#")
134		b.WriteString(ui.Inline(t))
135		b.WriteString("](")
136		b.WriteString(linkPrefix)
137		b.WriteString("t/")
138		b.WriteString(t)
139		b.WriteString(")")
140	}
141	return b.String()
142}
143
144func hasTag(p *post, tag string) bool {
145	for _, t := range splitTags(p.tags) {
146		if t == tag {
147			return true
148		}
149	}
150	return false
151}
152
153// firstLine returns the first non-empty, non-heading line of a body, which is
154// what an excerpt should show: a post that opens with "## Something" would
155// otherwise preview its own heading.
156func firstLine(body string) string {
157	for _, line := range strings.Split(body, "\n") {
158		line = strings.TrimSpace(line)
159		if line == "" || strings.HasPrefix(line, "#") {
160			continue
161		}
162		return line
163	}
164	return ""
165}