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}