package blog import ( "net/url" "strings" "gno.land/p/lou/query" "gno.land/p/moul/md" "gno.land/p/nt/avl/v0" "gno.land/p/nt/avl/v0/pager" "gno.land/p/nt/mux/v0" "gno.land/p/nt/ufmt/v0" ) func (b Blog) Render(path string) string { router := mux.NewRouter() router.HandleFunc("", b.RenderPosts) router.HandleFunc("posts", b.RenderPosts) router.HandleFunc("posts/{slug}", b.RenderPost) router.HandleFunc("tags", b.RenderTags) router.HandleFunc("tags/{slug}", b.RenderTags) router.HandleFunc("authors", b.RenderAuthors) router.HandleFunc("authors/{slug}", b.RenderAuthors) router.HandleFunc("commenters", b.RenderCommenters) router.HandleFunc("commenters/{slug}", b.RenderCommenters) return router.Render(path) } func RenderBlogHeader(prefix, rawPath string) string { rawPath = prefix + ":" + rawPath headerPresets := []HeaderPreset{ {"mode", "grid", RenderModeToggle}, {"time", "relative", RenderTimeFormat}, {"alpha", "", RenderSortAlpha}, {"recent", "", RenderSortRecent}, {"update", "", RenderSortUpdate}, {"time-range", "", func(p string) string { return RenderTimeRangeLinks(p, nil) }}, {"reset", "", func(p string) string { return md.Link("⟳ reset", prefix) }}, } return RenderHeader(rawPath, headerPresets) } // :posts func (b Blog) RenderPosts(res *mux.ResponseWriter, req *mux.Request) { out := md.H1(b.Title()) + "\n" out += RenderBlogHeader(b.Prefix(), req.RawPath) + "\n\n" if b.Posts.Size() == 0 { res.Write(out + "No posts found.") return } options := ParseRenderOptions(req.RawPath) pageSize := 9 if !options.IsGrid { pageSize = 3 } order := options.Ascending if options.IsAlphabetical { order = !order } var tree *avl.Tree switch { case options.IsLastUpdated: tree = b.PostsByUpdatedAt case options.IsAlphabetical: tree = b.PostsBySlug default: tree = b.Posts } if options.StartTime != nil || options.EndTime != nil { tree = b.filterPostsStartEnd(tree, options.StartTime, options.EndTime) } p := pager.NewPager(tree, pageSize, order) page := p.MustGetPageByPath(req.RawPath) out += b.RenderListGrid(page, options.IsGrid, options.TimeFormat) + page.Picker(req.Path) res.Write(out) } func (b Blog) RenderListGrid(page *pager.Page, gridMode bool, timeFmt string) string { colCount := 0 out := "\n" for _, item := range page.Items { if colCount%3 == 0 { out += "\n" } post := item.Value.(*Post) out += post.RenderPreview(gridMode, timeFmt, b.Prefix()) colCount++ if colCount%3 == 0 { out += "\n" } else if gridMode { out += "|||\n" } } if colCount%3 != 0 { out += "\n" } return out } // :posts/{slug} func (b Blog) RenderPost(res *mux.ResponseWriter, req *mux.Request) { postSlug := req.GetVar("slug") foundKey, found := b.findPostBySlug(url.PathEscape(postSlug)) if !found { res.Write("Post not found.") return } post, found := b.Posts.Get(foundKey) out := post.(*Post).RenderPost(b.Prefix()) res.Write(out) } func (b Blog) RenderFilterHeader(req *mux.Request, user string, isListing bool) string { out := "" field := strings.Split(req.RawPath, "/")[0] switch field { case "commenters": out += md.H2(md.Link(b.Prefix(), b.Prefix()) + "/commenters/" + user) out += md.H4(md.Link("@"+user, "/u/"+user) + "'s profile") case "authors": out += md.H2(renderBreadcrumb(b.Prefix(), req.Path)) if user != "" { out += md.H4(md.Link("@"+user, "/u/"+user) + "'s profile") } default: out += md.H2(renderBreadcrumb(b.Prefix(), req.Path)) } var headerPresets []HeaderPreset if !isListing { headerPresets = append(headerPresets, HeaderPreset{"mode", "grid", RenderModeToggle}, HeaderPreset{"time", "relative", RenderTimeFormat}, ) } headerPresets = append(headerPresets, HeaderPreset{"alpha", "", RenderSortAlpha}, HeaderPreset{"recent", "", RenderSortRecent}, ) if isListing { headerPresets = append(headerPresets, HeaderPreset{"common", "", RenderSortCommon}) } else { headerPresets = append(headerPresets, HeaderPreset{"update", "", RenderSortUpdate}) } headerPresets = append(headerPresets, HeaderPreset{"time-range", "", func(p string) string { return RenderTimeRangeLinks(p, nil) }}, HeaderPreset{"reset", "", func(p string) string { return md.Link("⟳ reset", b.Prefix()) }}, ) out += RenderHeader(b.Prefix()+":"+req.RawPath, headerPresets) return out + "\n\n" } // :tags func (b Blog) RenderTags(res *mux.ResponseWriter, req *mux.Request) { out := b.RenderListings("tag", req.GetVar("slug"), req) res.Write(out) } // :authors func (b Blog) RenderAuthors(res *mux.ResponseWriter, req *mux.Request) { out := b.RenderListings("author", req.GetVar("slug"), req) res.Write(out) } // :commenters func (b Blog) RenderCommenters(res *mux.ResponseWriter, req *mux.Request) { if req.GetVar("slug") == "" { res.Write("Commenter slug is required.") return } out := b.RenderListings("commenter", req.GetVar("slug"), req) res.Write(out) } func (b Blog) RenderListings(field, value string, req *mux.Request) string { options := ParseRenderOptions(req.RawPath) if query.GetQuery("mode", req.RawPath) == "" { options.IsGrid = false } if value == "" { return b.renderListingIndex(field, options, req) } user, err := CheckUser(value, b.UserResolver) if err == nil && user != "" { return b.renderFilteredListing(field, user, options, req) } return b.renderFilteredListing(field, value, options, req) } func (b Blog) renderListingIndex(field string, options RenderOptions, req *mux.Request) string { out := b.RenderFilterHeader(req, "", true) p, exists := b.determinePager(options, field, "") if !exists { return out + "No " + field + "s found." } page := p.MustGetPageByPath(req.RawPath) for _, item := range page.Items { label, count := extractListingDisplay(item.Key, item.Value) out += md.H3(md.Link(label, b.Prefix()+":"+field+"s/"+label)+" ("+ufmt.Sprintf("%d", count)+")") + "\n\n" } out += page.Picker(req.Path) + "\n\n" return out } func (b Blog) renderFilteredListing(field, value string, options RenderOptions, req *mux.Request) string { out := b.RenderFilterHeader(req, value, false) p, exists := b.determinePager(options, field, value) if !exists { out += "No posts found for this " + field + "." } page := p.MustGetPageByPath(req.RawPath) out += "\n" colCount := 0 for _, item := range page.Items { if colCount%3 == 0 { out += "\n" } colCount++ if field == "commenter" { out += b.renderCommenterItem(item, options) } else { post := item.Value.(*Post) out += post.RenderPreview(options.IsGrid, options.TimeFormat, b.Prefix()) + "\n" } if colCount%3 == 0 { out += "\n" } else if options.IsGrid { out += "|||\n" } } if colCount%3 != 0 { out += "\n" } out += page.Picker(req.Path) + "\n\n" return out } func (b Blog) renderCommenterItem(item pager.Item, options RenderOptions) string { comment := item.Value.(*Comment) parts := strings.Split(item.Key, "::") if len(parts) < 2 { return "" } postKey := parts[1] var post interface{} var found bool if post, found = b.Posts.Get(postKey); !found { if post, found = b.PostsBySlug.Get(postKey); !found { post, found = b.PostsByUpdatedAt.Get(postKey) } } if !found { return "" } p := post.(*Post) user, _ := CheckUser(comment.Author(), b.UserResolver) out := md.H4(b.Mention("commenter", user)) + "\n\n" out += comment.Content() + "\n\n" out += md.Italic(formatTime(comment.CreatedAt(), options.TimeFormat)) + "\n\n" out += "in " + md.Link(p.Title(), b.Prefix()+":posts/"+p.Slug()) + "\n\n" out += md.HorizontalRule() return out } func (b Blog) determinePager(options RenderOptions, field, value string) (*pager.Pager, bool) { if value == "" { if options.IsAlphabetical && field == "author" { return pager.NewPager(b.AuthorsIndex, 12, !options.Ascending), true } else if options.IsCommon && field == "author" { return pager.NewPager(b.AuthorsSorted, 12, options.Ascending), true } else if options.IsAlphabetical && field == "tag" { return pager.NewPager(b.TagsIndex, 12, !options.Ascending), true } else if options.IsCommon && field == "tag" { return pager.NewPager(b.TagsSorted, 12, options.Ascending), true } else if field == "author" { return pager.NewPager(b.AuthorsIndex, 12, options.Ascending), true } else if field == "tag" { return pager.NewPager(b.TagsIndex, 12, options.Ascending), true } } pageSize := 9 if !options.IsGrid { pageSize = 3 } filteredPosts, exists := b.filterPostsByField(field, value, options.Sort) if options.StartTime != nil || options.EndTime != nil { filteredPosts = b.filterPostsStartEnd(filteredPosts, options.StartTime, options.EndTime) } if options.Sort == "alpha" { return pager.NewPager(filteredPosts, pageSize, !options.Ascending), exists } return pager.NewPager(filteredPosts, pageSize, options.Ascending), exists } func orderArrow(targetSort, currentSort, order string) string { if targetSort != currentSort { return "↕" } if order == "asc" { return "▲" } return "▼" }