Search Apps Documentation Source Content File Folder Download Copy Actions Download

render.gno

8.92 Kb · 312 lines
  1package blog
  2
  3import (
  4	"net/url"
  5	"strings"
  6
  7	"gno.land/p/lou/query"
  8	"gno.land/p/moul/md"
  9	"gno.land/p/nt/avl"
 10	"gno.land/p/nt/avl/pager"
 11	"gno.land/p/nt/mux"
 12	"gno.land/p/nt/ufmt"
 13)
 14
 15func (b Blog) Render(path string) string {
 16	router := mux.NewRouter()
 17	router.HandleFunc("", b.RenderPosts)
 18	router.HandleFunc("posts", b.RenderPosts)
 19	router.HandleFunc("posts/{slug}", b.RenderPost)
 20	router.HandleFunc("tags", b.RenderTags)
 21	router.HandleFunc("tags/{slug}", b.RenderTags)
 22	router.HandleFunc("authors", b.RenderAuthors)
 23	router.HandleFunc("authors/{slug}", b.RenderAuthors)
 24	router.HandleFunc("commenters", b.RenderCommenters)
 25	router.HandleFunc("commenters/{slug}", b.RenderCommenters)
 26	return router.Render(path)
 27}
 28
 29func RenderBlogHeader(prefix, rawPath string) string {
 30	rawPath = prefix + ":" + rawPath
 31	headerPresets := []HeaderPreset{
 32		{"mode", "grid", RenderModeToggle},
 33		{"time", "relative", RenderTimeFormat},
 34		{"alpha", "", RenderSortAlpha},
 35		{"recent", "", RenderSortRecent},
 36		{"update", "", RenderSortUpdate},
 37		{"time-range", "", func(p string) string { return RenderTimeRangeLinks(p, nil) }},
 38		{"reset", "", func(p string) string { return md.Link("⟳ reset", prefix) }},
 39	}
 40	return RenderHeader(rawPath, headerPresets)
 41}
 42
 43// :posts
 44func (b Blog) RenderPosts(res *mux.ResponseWriter, req *mux.Request) {
 45	out := md.H1(b.Title()) + "\n"
 46	out += RenderBlogHeader(b.Prefix(), req.RawPath) + "\n\n"
 47	if b.Posts.Size() == 0 {
 48		res.Write(out + "No posts found.")
 49		return
 50	}
 51
 52	options := ParseRenderOptions(req.RawPath)
 53	pageSize := 9
 54	if !options.IsGrid {
 55		pageSize = 3
 56	}
 57	order := options.Ascending
 58	if options.IsAlphabetical {
 59		order = !order
 60	}
 61
 62	var tree *avl.Tree
 63	switch {
 64	case options.IsLastUpdated:
 65		tree = b.PostsByUpdatedAt
 66	case options.IsAlphabetical:
 67		tree = b.PostsBySlug
 68	default:
 69		tree = b.Posts
 70	}
 71	if options.StartTime != nil || options.EndTime != nil {
 72		tree = b.filterPostsStartEnd(tree, options.StartTime, options.EndTime)
 73	}
 74
 75	p := pager.NewPager(tree, pageSize, order)
 76	page := p.MustGetPageByPath(req.RawPath)
 77	out += b.RenderListGrid(page, options.IsGrid, options.TimeFormat) + page.Picker(req.Path)
 78	res.Write(out)
 79}
 80
 81func (b Blog) RenderListGrid(page *pager.Page, gridMode bool, timeFmt string) string {
 82	colCount := 0
 83	out := "<gno-columns>\n"
 84	for _, item := range page.Items {
 85		if colCount%3 == 0 {
 86			out += "<gno-columns>\n"
 87		}
 88		post := item.Value.(*Post)
 89		out += post.RenderPreview(gridMode, timeFmt, b.Prefix())
 90		colCount++
 91		if colCount%3 == 0 {
 92			out += "</gno-columns>\n"
 93		} else if gridMode {
 94			out += "|||\n"
 95		}
 96	}
 97	if colCount%3 != 0 {
 98		out += "</gno-columns>\n"
 99	}
100	return out
101}
102
103// :posts/{slug}
104func (b Blog) RenderPost(res *mux.ResponseWriter, req *mux.Request) {
105	postSlug := req.GetVar("slug")
106	foundKey, found := b.findPostBySlug(url.PathEscape(postSlug))
107	if !found {
108		res.Write("Post not found.")
109		return
110	}
111	post, found := b.Posts.Get(foundKey)
112	out := post.(*Post).RenderPost(b.Prefix())
113
114	res.Write(out)
115}
116
117func (b Blog) RenderFilterHeader(req *mux.Request, user string, isListing bool) string {
118	out := ""
119	field := strings.Split(req.RawPath, "/")[0]
120	switch field {
121	case "commenters":
122		out += md.H2(md.Link(b.Prefix(), b.Prefix()) + "/commenters/" + user)
123		out += md.H4(md.Link("@"+user, "/u/"+user) + "'s profile")
124	case "authors":
125		out += md.H2(renderBreadcrumb(b.Prefix(), req.Path))
126		if user != "" {
127			out += md.H4(md.Link("@"+user, "/u/"+user) + "'s profile")
128		}
129	default:
130		out += md.H2(renderBreadcrumb(b.Prefix(), req.Path))
131	}
132
133	var headerPresets []HeaderPreset
134	if !isListing {
135		headerPresets = append(headerPresets,
136			HeaderPreset{"mode", "grid", RenderModeToggle},
137			HeaderPreset{"time", "relative", RenderTimeFormat},
138		)
139	}
140	headerPresets = append(headerPresets,
141		HeaderPreset{"alpha", "", RenderSortAlpha},
142		HeaderPreset{"recent", "", RenderSortRecent},
143	)
144
145	if isListing {
146		headerPresets = append(headerPresets, HeaderPreset{"common", "", RenderSortCommon})
147	} else {
148		headerPresets = append(headerPresets, HeaderPreset{"update", "", RenderSortUpdate})
149	}
150	headerPresets = append(headerPresets,
151		HeaderPreset{"time-range", "", func(p string) string { return RenderTimeRangeLinks(p, nil) }},
152		HeaderPreset{"reset", "", func(p string) string { return md.Link("⟳ reset", b.Prefix()) }},
153	)
154	out += RenderHeader(b.Prefix()+":"+req.RawPath, headerPresets)
155	return out + "\n\n"
156}
157
158// :tags
159func (b Blog) RenderTags(res *mux.ResponseWriter, req *mux.Request) {
160	out := b.RenderListings("tag", req.GetVar("slug"), req)
161	res.Write(out)
162}
163
164// :authors
165func (b Blog) RenderAuthors(res *mux.ResponseWriter, req *mux.Request) {
166	out := b.RenderListings("author", req.GetVar("slug"), req)
167	res.Write(out)
168}
169
170// :commenters
171func (b Blog) RenderCommenters(res *mux.ResponseWriter, req *mux.Request) {
172	if req.GetVar("slug") == "" {
173		res.Write("Commenter slug is required.")
174		return
175	}
176	out := b.RenderListings("commenter", req.GetVar("slug"), req)
177	res.Write(out)
178}
179
180func (b Blog) RenderListings(field, value string, req *mux.Request) string {
181	options := ParseRenderOptions(req.RawPath)
182	if query.GetQuery("mode", req.RawPath) == "" {
183		options.IsGrid = false
184	}
185	if value == "" {
186		return b.renderListingIndex(field, options, req)
187	}
188	user, err := CheckUser(value, b.UserResolver)
189	if err == nil && user != "" {
190		return b.renderFilteredListing(field, user, options, req)
191	}
192	return b.renderFilteredListing(field, value, options, req)
193}
194
195func (b Blog) renderListingIndex(field string, options RenderOptions, req *mux.Request) string {
196	out := b.RenderFilterHeader(req, "", true)
197	p, exists := b.determinePager(options, field, "")
198	if !exists {
199		return out + "No " + field + "s found."
200	}
201	page := p.MustGetPageByPath(req.RawPath)
202
203	for _, item := range page.Items {
204		label, count := extractListingDisplay(item.Key, item.Value)
205		out += md.H3(md.Link(label, b.Prefix()+":"+field+"s/"+label)+" ("+ufmt.Sprintf("%d", count)+")") + "\n\n"
206	}
207	out += page.Picker(req.Path) + "\n\n"
208	return out
209}
210
211func (b Blog) renderFilteredListing(field, value string, options RenderOptions, req *mux.Request) string {
212	out := b.RenderFilterHeader(req, value, false)
213	p, exists := b.determinePager(options, field, value)
214	if !exists {
215		out += "No posts found for this " + field + "."
216	}
217	page := p.MustGetPageByPath(req.RawPath)
218	out += "<gno-columns>\n"
219	colCount := 0
220	for _, item := range page.Items {
221		if colCount%3 == 0 {
222			out += "<gno-columns>\n"
223		}
224		colCount++
225		if field == "commenter" {
226			out += b.renderCommenterItem(item, options)
227		} else {
228			post := item.Value.(*Post)
229			out += post.RenderPreview(options.IsGrid, options.TimeFormat, b.Prefix()) + "\n"
230		}
231		if colCount%3 == 0 {
232			out += "</gno-columns>\n"
233		} else if options.IsGrid {
234			out += "|||\n"
235		}
236	}
237	if colCount%3 != 0 {
238		out += "</gno-columns>\n"
239	}
240	out += page.Picker(req.Path) + "\n\n"
241	return out
242}
243
244func (b Blog) renderCommenterItem(item pager.Item, options RenderOptions) string {
245	comment := item.Value.(*Comment)
246	parts := strings.Split(item.Key, "::")
247	if len(parts) < 2 {
248		return ""
249	}
250	postKey := parts[1]
251
252	var post interface{}
253	var found bool
254	if post, found = b.Posts.Get(postKey); !found {
255		if post, found = b.PostsBySlug.Get(postKey); !found {
256			post, found = b.PostsByUpdatedAt.Get(postKey)
257		}
258	}
259	if !found {
260		return ""
261	}
262	p := post.(*Post)
263
264	user, _ := CheckUser(comment.Author(), b.UserResolver)
265	out := md.H4(b.Mention("commenter", user)) + "\n\n"
266	out += comment.Content() + "\n\n"
267	out += md.Italic(formatTime(comment.CreatedAt(), options.TimeFormat)) + "\n\n"
268	out += "in " + md.Link(p.Title(), b.Prefix()+":posts/"+p.Slug()) + "\n\n"
269	out += md.HorizontalRule()
270	return out
271}
272
273func (b Blog) determinePager(options RenderOptions, field, value string) (*pager.Pager, bool) {
274	if value == "" {
275		if options.IsAlphabetical && field == "author" {
276			return pager.NewPager(b.AuthorsIndex, 12, !options.Ascending), true
277		} else if options.IsCommon && field == "author" {
278			return pager.NewPager(b.AuthorsSorted, 12, options.Ascending), true
279		} else if options.IsAlphabetical && field == "tag" {
280			return pager.NewPager(b.TagsIndex, 12, !options.Ascending), true
281		} else if options.IsCommon && field == "tag" {
282			return pager.NewPager(b.TagsSorted, 12, options.Ascending), true
283		} else if field == "author" {
284			return pager.NewPager(b.AuthorsIndex, 12, options.Ascending), true
285		} else if field == "tag" {
286			return pager.NewPager(b.TagsIndex, 12, options.Ascending), true
287		}
288	}
289	pageSize := 9
290	if !options.IsGrid {
291		pageSize = 3
292	}
293
294	filteredPosts, exists := b.filterPostsByField(field, value, options.Sort)
295	if options.StartTime != nil || options.EndTime != nil {
296		filteredPosts = b.filterPostsStartEnd(filteredPosts, options.StartTime, options.EndTime)
297	}
298	if options.Sort == "alpha" {
299		return pager.NewPager(filteredPosts, pageSize, !options.Ascending), exists
300	}
301	return pager.NewPager(filteredPosts, pageSize, options.Ascending), exists
302}
303
304func orderArrow(targetSort, currentSort, order string) string {
305	if targetSort != currentSort {
306		return "↕"
307	}
308	if order == "asc" {
309		return "▲"
310	}
311	return "▼"
312}